diff --git a/AUTO_LOGGING_GUIDE.md b/AUTO_LOGGING_GUIDE.md new file mode 100644 index 0000000..c440798 --- /dev/null +++ b/AUTO_LOGGING_GUIDE.md @@ -0,0 +1,189 @@ +# Automatic Activity Logging Guide + +## Overview + +The Todo App Backend now includes automatic activity logging for all CRUD operations (Create, Read, Update, Delete) on key entities. This is implemented using a PHP trait that can be easily added to any model. + +## How It Works + +### LoggableTrait + +The `LoggableTrait` in `app/Models/LoggableTrait.php` provides automatic logging functionality by hooking into CodeIgniter 4's model lifecycle events: + +- **afterInsert**: Logs when a new record is created +- **afterUpdate**: Logs when a record is updated +- **afterDelete**: Logs when a record is deleted + +### Enabled Models + +The following models now have automatic logging enabled: + +1. **UserModel** - Logs user creation, updates, and deletion +2. **TodoModel** - Logs todo creation, updates, and deletion +3. **CategoryModel** - Logs category creation, updates, and deletion +4. **ProjectModel** - Logs project creation, updates, and deletion +5. **RecurringTaskModel** - Logs recurring task creation, updates, and deletion + +### What Gets Logged + +Each log entry includes: + +- **user_id**: The ID of the user performing the action (from the record or session) +- **action**: Formatted action name (e.g., "todo_created", "user_updated", "category_deleted") +- **entity_type**: The type of entity (e.g., "todo", "user", "category") +- **entity_id**: The ID of the affected entity +- **details**: JSON object with relevant fields (title, name, email, etc.) +- **ip_address**: Client IP address +- **user_agent**: Browser user agent string +- **created_at**: Timestamp of the action + +### Example Log Entries + +**Creating a todo:** +```json +{ + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "action": "todo_created", + "entity_type": "todo", + "entity_id": "550e8400-e29b-41d4-a716-446655440001", + "details": { + "action": "created", + "title": "Complete project documentation" + }, + "ip_address": "192.168.1.100", + "user_agent": "Mozilla/5.0...", + "created_at": "2026-04-29 13:42:00" +} +``` + +**Updating a user:** +```json +{ + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "action": "user_updated", + "entity_type": "user", + "entity_id": "550e8400-e29b-41d4-a716-446655440000", + "details": { + "action": "updated", + "name": "John Doe", + "email": "john@example.com" + }, + "ip_address": "192.168.1.100", + "user_agent": "Mozilla/5.0...", + "created_at": "2026-04-29 13:45:00" +} +``` + +**Deleting a category:** +```json +{ + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "action": "category_deleted", + "entity_type": "category", + "entity_id": "550e8400-e29b-41d4-a716-446655440002", + "details": { + "action": "deleted", + "name": "Work" + }, + "ip_address": "192.168.1.100", + "user_agent": "Mozilla/5.0...", + "created_at": "2026-04-29 13:50:00" +} +``` + +## Adding Logging to Other Models + +To add automatic logging to a new model: + +1. Add the `use LoggableTrait;` statement to your model class +2. Override the `getEntityType()` method to return the entity type name + +Example: +```php +getByUser($userId); + +// Get logs for a specific entity +$logs = $activityLogModel->getByEntity('todo', $todoId); + +// Get logs by action type +$logs = $activityLogModel->getByAction('todo_created'); + +// Custom query +$logs = $activityLogModel + ->where('user_id', $userId) + ->where('entity_type', 'todo') + ->orderBy('created_at', 'DESC') + ->limit(20) + ->get() + ->getResultArray(); +``` + +## Session Requirement + +The logging system attempts to get the user ID from: +1. The data being inserted/updated (e.g., `user_id` field in the record) +2. The session (`session()->get('user_id')`) + +Make sure your authentication system sets the user ID in the session for proper logging. + +## Disabling Logging + +To disable automatic logging for a specific operation, you can temporarily disable the model events: + +```php +$todoModel = new TodoModel(); +$todoModel->disableEvents(); +$todoModel->insert($data); +$todoModel->enableEvents(); +``` + +Or remove the `use LoggableTrait;` from the model class entirely. diff --git a/DATABASE_DOCUMENTATION.md b/DATABASE_DOCUMENTATION.md new file mode 100644 index 0000000..7426cfb --- /dev/null +++ b/DATABASE_DOCUMENTATION.md @@ -0,0 +1,548 @@ +# Todo App Backend - Database Documentation + +## Overview + +This document describes the database schema for the Todo App Backend, built with CodeIgniter 4 and MySQL. The database supports user accounts, todo management, recurring tasks, activity logging, theme marketplace, and AI-powered chat features. + +## Database Schema + +### Core Tables + +#### 1. users +Stores user account information and application settings. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| email | VARCHAR(255) | NO | User email (unique) | +| password_hash | VARCHAR(255) | NO | Bcrypt hashed password | +| name | VARCHAR(255) | YES | Display name | +| avatar_url | TEXT | YES | Profile image URL | +| settings | JSON | YES | App preferences (language, default view, etc.) | +| created_at | DATETIME | YES | Creation timestamp | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- UNIQUE KEY (email) + +--- + +#### 2. categories +Per-user categories for organizing todos. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| name | VARCHAR(255) | NO | Category name | +| color | VARCHAR(7) | YES | Hex color code for UI | +| favorite | BOOLEAN | NO | Mark as favorite | +| created_at | DATETIME | YES | Creation timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) +- UNIQUE KEY (user_id, name) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE + +--- + +#### 3. projects +Optional project grouping for todos. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| name | VARCHAR(255) | NO | Project name | +| description | TEXT | YES | Project description | +| color | VARCHAR(7) | YES | Hex color code for UI | +| created_at | DATETIME | YES | Creation timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE + +--- + +#### 4. todos +Main todo items. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| title | VARCHAR(255) | NO | Todo title | +| description | TEXT | YES | Detailed description | +| status | ENUM | NO | open, in_progress, completed, archived | +| due_date | DATE | YES | Due date | +| due_time | TIME | YES | Due time | +| sync_enabled | BOOLEAN | NO | Sync with external services | +| reminder_enabled | BOOLEAN | NO | Enable reminders | +| recurring_enabled | BOOLEAN | NO | Mark as recurring | +| project_id | CHAR(36) | YES | Foreign key to projects | +| created_at | DATETIME | YES | Creation timestamp | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) +- KEY (due_date) +- KEY (status) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE +- project_id → projects(id) ON DELETE SET NULL + +--- + +#### 5. todo_categories (Junction Table) +Many-to-many relationship between todos and categories. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| todo_id | CHAR(36) | NO | Foreign key to todos | +| category_id | CHAR(36) | NO | Foreign key to categories | + +**Indexes:** +- PRIMARY KEY (todo_id, category_id) + +**Foreign Keys:** +- todo_id → todos(id) ON DELETE CASCADE +- category_id → categories(id) ON DELETE CASCADE + +--- + +#### 6. recurring_tasks +Templates for recurring todo items. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| title | VARCHAR(255) | NO | Task title | +| description | TEXT | YES | Task description | +| schedule | ENUM | NO | daily, weekly, monthly, custom | +| custom_days | JSON | YES | Array of days (e.g., ["mon","wed","fri"]) | +| favorite | BOOLEAN | NO | Mark as favorite | +| created_at | DATETIME | YES | Creation timestamp | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE + +--- + +#### 7. recurring_task_categories (Junction Table) +Many-to-many relationship between recurring tasks and categories. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| recurring_task_id | CHAR(36) | NO | Foreign key to recurring_tasks | +| category_id | CHAR(36) | NO | Foreign key to categories | + +**Indexes:** +- PRIMARY KEY (recurring_task_id, category_id) + +**Foreign Keys:** +- recurring_task_id → recurring_tasks(id) ON DELETE CASCADE +- category_id → categories(id) ON DELETE CASCADE + +--- + +### Activity Logging + +#### 8. activity_logs +Audit trail for user actions and system events. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | YES | Foreign key to users (nullable for anonymous) | +| action | VARCHAR(255) | NO | Action type (e.g., todo_created, login) | +| entity_type | VARCHAR(100) | YES | Entity type (todo, category, project, etc.) | +| entity_id | CHAR(36) | YES | Entity ID | +| details | JSON | YES | Additional metadata (before/after values) | +| ip_address | VARCHAR(45) | YES | User IP address | +| user_agent | TEXT | YES | Browser user agent | +| created_at | DATETIME | YES | Creation timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) +- KEY (created_at) +- KEY (action) +- KEY (entity_type, entity_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE SET NULL + +--- + +### Theme Marketplace + +#### 9. marketplace_themes +Master list of available themes in the marketplace. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| name | VARCHAR(255) | NO | Theme identifier (unique) | +| display_name | VARCHAR(255) | NO | Human-readable name | +| description | TEXT | YES | Theme description | +| author | VARCHAR(255) | YES | Theme author | +| version | VARCHAR(50) | YES | Theme version | +| thumbnail_url | TEXT | YES | Preview image URL | +| download_url | TEXT | NO | Download URL | +| price | DECIMAL(10,2) | NO | Theme price (0 = free) | +| is_published | BOOLEAN | NO | Published status | +| metadata | JSON | YES | Tags, screenshots, etc. | +| created_at | DATETIME | YES | Creation timestamp | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- UNIQUE KEY (name) + +--- + +#### 10. user_themes +Themes installed by users. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| theme_id | CHAR(36) | NO | Foreign key to marketplace_themes | +| installed_at | DATETIME | YES | Installation timestamp | +| active | BOOLEAN | NO | Currently active theme | +| custom_settings | JSON | YES | User theme overrides | + +**Indexes:** +- PRIMARY KEY (id) +- UNIQUE KEY (user_id, theme_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE +- theme_id → marketplace_themes(id) ON DELETE CASCADE + +--- + +### AI Features + +#### 11. ai_providers +Supported AI providers (OpenAI, Anthropic, Google, etc.). + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| name | VARCHAR(100) | NO | Provider identifier (unique) | +| display_name | VARCHAR(255) | NO | Human-readable name | +| base_url | TEXT | YES | API endpoint override | +| is_builtin | BOOLEAN | NO | System vs custom provider | +| created_at | DATETIME | YES | Creation timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- UNIQUE KEY (name) + +--- + +#### 12. user_api_keys +Encrypted API keys for each provider per user. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| provider_id | CHAR(36) | NO | Foreign key to ai_providers | +| api_key_encrypted | TEXT | NO | Encrypted API key | +| label | VARCHAR(255) | YES | Key label (e.g., "Work Key") | +| is_active | BOOLEAN | NO | Active status | +| created_at | DATETIME | YES | Creation timestamp | +| last_used_at | DATETIME | YES | Last usage timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- UNIQUE KEY (user_id, provider_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE +- provider_id → ai_providers(id) ON DELETE CASCADE + +--- + +#### 13. user_ai_settings +Per-user AI preferences. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| user_id | CHAR(36) | NO | Primary key, Foreign key to users | +| default_provider_id | CHAR(36) | YES | Foreign key to ai_providers | +| default_model | VARCHAR(100) | YES | Default model (e.g., gpt-4) | +| max_tokens | INT | NO | Maximum tokens (default: 2048) | +| temperature | FLOAT | NO | Temperature (default: 0.7) | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (user_id) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE +- default_provider_id → ai_providers(id) ON DELETE SET NULL + +--- + +#### 14. ai_chats +AI conversation threads. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| user_id | CHAR(36) | NO | Foreign key to users | +| title | VARCHAR(255) | YES | Chat title | +| provider_id | CHAR(36) | YES | Foreign key to ai_providers | +| model_used | VARCHAR(100) | YES | Model snapshot | +| system_prompt | TEXT | YES | Custom system prompt | +| created_at | DATETIME | YES | Creation timestamp | +| updated_at | DATETIME | YES | Last update timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (user_id) +- KEY (updated_at) + +**Foreign Keys:** +- user_id → users(id) ON DELETE CASCADE +- provider_id → ai_providers(id) ON DELETE SET NULL + +--- + +#### 15. ai_messages +Individual messages in AI chats. + +| Column | Type | Nullable | Description | +|--------|------|----------|-------------| +| id | CHAR(36) | NO | Primary key (UUID) | +| chat_id | CHAR(36) | NO | Foreign key to ai_chats | +| role | ENUM | NO | user, assistant, system | +| content | TEXT | NO | Message content | +| tokens_used | INT | YES | Token count for billing | +| created_at | DATETIME | YES | Creation timestamp | + +**Indexes:** +- PRIMARY KEY (id) +- KEY (chat_id) + +**Foreign Keys:** +- chat_id → ai_chats(id) ON DELETE CASCADE + +--- + +## Entity Relationship Diagram (ERD) + +``` +┌─────────────────┐ +│ users │ +├─────────────────┤ +│ id (PK) │◄────────┐ +│ email │ │ +│ password_hash │ │ +│ name │ │ +│ settings │ │ +│ created_at │ │ +│ updated_at │ │ +└─────────────────┘ │ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + │ │ │ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ categories │ │ projects │ │ activity_logs │ +├───────────────┤ ├───────────────┤ ├───────────────┤ +│ id (PK) │ │ id (PK) │ │ id (PK) │ +│ user_id (FK) │ │ user_id (FK) │ │ user_id (FK) │ +│ name │ │ name │ │ action │ +│ color │ │ description │ │ entity_type │ +│ favorite │ │ color │ │ entity_id │ +│ created_at │ │ created_at │ │ details │ +└───────────────┘ └───────────────┘ │ ip_address │ + │ │ │ user_agent │ + │ │ │ created_at │ + │ │ └───────────────┘ + │ │ + │ │ + │ │ +┌───────────────┐ ┌───────────────┐ +│ todos │ │recurring_tasks│ +├───────────────┤ ├───────────────┤ +│ id (PK) │ │ id (PK) │ +│ user_id (FK) │ │ user_id (FK) │ +│ title │ │ title │ +│ description │ │ description │ +│ status │ │ schedule │ +│ due_date │ │ custom_days │ +│ due_time │ │ favorite │ +│ project_id(FK)│ │ created_at │ +│ created_at │ │ updated_at │ +│ updated_at │ └───────────────┘ +└───────────────┘ │ + │ │ + │ │ + │ │ +┌──────────────────┐ │ +│ todo_categories │◄───────┘ +├──────────────────┤ +│ todo_id (PK,FK) │ +│ category_id(PK,FK)│ +└──────────────────┘ + │ + │ +┌──────────────────────────┐ +│recurring_task_categories │ +├──────────────────────────┤ +│ recurring_task_id (PK,FK) │ +│ category_id (PK,FK) │ +└──────────────────────────┘ + +┌─────────────────┐ +│marketplace_themes│ +├─────────────────┤ +│ id (PK) │◄────────┐ +│ name │ │ +│ display_name │ │ +│ description │ │ +│ download_url │ │ +│ price │ │ +│ is_published │ │ +└─────────────────┘ │ + │ + │ + ┌─────────┴─────────┐ + │ user_themes │ + ├──────────────────┤ + │ id (PK) │ + │ user_id (FK) │ + │ theme_id (FK) │ + │ active │ + │ custom_settings │ + └──────────────────┘ + +┌─────────────┐ +│ai_providers │ +├─────────────┤ +│ id (PK) │◄────────┐ +│ name │ │ +│ display_name│ │ +│ base_url │ │ +│ is_builtin │ │ +└─────────────┘ │ + │ + ┌───────────────┼──────────────┐ + │ │ │ +┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ +│ user_api_keys │ │user_ai_settin│ │ ai_chats │ +├────────────────┤ ├──────────────┤ ├─────────────────┤ +│ id (PK) │ │user_id (PK) │ │ id (PK) │ +│ user_id (FK) │ │default_prv(FK)│ │ user_id (FK) │ +│ provider_id(FK) │ │default_model │ │ provider_id(FK) │ +│ api_key_enc │ │max_tokens │ │ title │ +│ label │ │temperature │ │ model_used │ +│ is_active │ │updated_at │ │ system_prompt │ +│ last_used_at │ └──────────────┘ │ created_at │ +└────────────────┘ │ updated_at │ + └─────────────────┘ + │ + │ + ┌────────────────┐ + │ ai_messages │ + ├────────────────┤ + │ id (PK) │ + │ chat_id (FK) │ + │ role │ + │ content │ + │ tokens_used │ + │ created_at │ + └────────────────┘ +``` + +## Relationships Summary + +| Entity | Relations | +|--------|-----------| +| **users** | Has many: todos, categories, projects, recurring_tasks, activity_logs, user_themes, user_api_keys, ai_chats, user_ai_settings | +| **todos** | Belongs to: user, project (optional). Many-to-many with categories via todo_categories | +| **recurring_tasks** | Belongs to: user. Many-to-many with categories via recurring_task_categories | +| **categories** | Linked to: todos, recurring_tasks | +| **marketplace_themes** | Installed by users via user_themes | +| **ai_providers** | Referenced by: user_api_keys, ai_chats, user_ai_settings | +| **ai_chats** | Belongs to: user, provider (optional). Has many: ai_messages | +| **ai_messages** | Belongs to: chat | + +## Key Design Decisions + +1. **UUID Primary Keys**: Using CHAR(36) for UUIDs to support distributed systems and prevent ID enumeration attacks. + +2. **Foreign Key Cascades**: + - CASCADE DELETE for user-owned entities to clean up data when users are deleted + - SET NULL for optional references (e.g., project_id in todos) + +3. **JSON Fields**: Used for flexible data like settings, custom_days, and metadata. + +4. **Junction Tables**: Proper normalization for many-to-many relationships (todo-categories, recurring_task-categories). + +5. **Activity Logging**: Nullable user_id allows for anonymous/system events. + +6. **Theme Marketplace**: Separation of global theme catalog and user installations. + +7. **AI Multi-Provider**: Support for multiple AI providers with per-user encrypted API keys. + +## Migration and Seeding + +To set up the database: + +```bash +# Run all migrations +php spark migrate + +# Run seeders +php spark db:seed AiProvidersSeeder +php spark db:seed MarketplaceThemesSeeder +php spark db:seed SampleDataSeeder +``` + +## Model Files + +All tables have corresponding CodeIgniter 4 models in `app/Models/`: + +- UserModel +- CategoryModel +- ProjectModel +- TodoModel +- TodoCategoryModel +- RecurringTaskModel +- RecurringTaskCategoryModel +- ActivityLogModel +- MarketplaceThemeModel +- UserThemeModel +- AiProviderModel +- UserApiKeyModel +- UserAiSettingsModel +- AiChatModel +- AiMessageModel + +Each model includes: +- Validation rules +- Timestamp handling +- Custom query methods for common operations +- Relationship helpers diff --git a/app/Commands/TestModels.php b/app/Commands/TestModels.php new file mode 100644 index 0000000..f23bee5 --- /dev/null +++ b/app/Commands/TestModels.php @@ -0,0 +1,93 @@ +where('email', 'demo@example.com')->first(); + if (!$user) { + CLI::write('✗ No demo user found. Please run seeders first.', 'red'); + return; + } + $userId = $user['id']; + CLI::write("✓ Using user: {$user['name']} ({$userId})", 'green'); + CLI::newLine(); + + // Test 2: Query categories + CLI::write('Test 2: Querying categories...', 'yellow'); + $categoryModel = new CategoryModel(); + $categories = $categoryModel->where('user_id', $userId)->findAll(); + CLI::write("✓ Found " . count($categories) . " categories for user", 'green'); + foreach ($categories as $cat) { + CLI::write(" - {$cat['name']} ({$cat['color']})", 'light_gray'); + } + CLI::newLine(); + + // Test 3: Query projects + CLI::write('Test 3: Querying projects...', 'yellow'); + $projectModel = new ProjectModel(); + $projects = $projectModel->where('user_id', $userId)->findAll(); + CLI::write("✓ Found " . count($projects) . " projects for user", 'green'); + foreach ($projects as $proj) { + CLI::write(" - {$proj['name']}", 'light_gray'); + } + CLI::newLine(); + + // Test 4: Query todos + CLI::write('Test 4: Querying todos...', 'yellow'); + $todoModel = new TodoModel(); + $todos = $todoModel->getByUserWithCategories($userId); + CLI::write("✓ Found " . count($todos) . " todos for user", 'green'); + foreach ($todos as $todo) { + CLI::write(" - {$todo['title']} ({$todo['status']})", 'light_gray'); + } + CLI::newLine(); + + // Test 5: Query recurring tasks + CLI::write('Test 5: Querying recurring tasks...', 'yellow'); + $recurringTaskModel = new RecurringTaskModel(); + $recurringTasks = $recurringTaskModel->getByUserWithCategories($userId); + CLI::write("✓ Found " . count($recurringTasks) . " recurring tasks for user", 'green'); + foreach ($recurringTasks as $task) { + CLI::write(" - {$task['title']} ({$task['schedule']})", 'light_gray'); + } + CLI::newLine(); + + CLI::write('=== All Tests Completed Successfully ===', 'green'); + CLI::write("Test User ID: {$userId}", 'light_gray'); + CLI::write('Models are working correctly. You can now use them in your controllers.', 'light_gray'); + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/app/Database/Migrations/2025-01-01-000000_CreateUsersTable.php b/app/Database/Migrations/2025-01-01-000000_CreateUsersTable.php new file mode 100644 index 0000000..b0cab7f --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000000_CreateUsersTable.php @@ -0,0 +1,59 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'email' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'password_hash' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + ], + 'avatar_url' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'settings' => [ + 'type' => 'JSON', + 'null' => true, + 'default' => '{}', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('email'); + $this->forge->createTable('users'); + } + + public function down() + { + $this->forge->dropTable('users'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000001_CreateCategoriesTable.php b/app/Database/Migrations/2025-01-01-000001_CreateCategoriesTable.php new file mode 100644 index 0000000..64bea85 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000001_CreateCategoriesTable.php @@ -0,0 +1,53 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'color' => [ + 'type' => 'VARCHAR', + 'constraint' => 7, + 'null' => true, + 'comment' => 'Hex color for UI', + ], + 'favorite' => [ + 'type' => 'BOOLEAN', + 'default' => false, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addUniqueKey(['user_id', 'name']); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('categories'); + } + + public function down() + { + $this->forge->dropTable('categories'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000002_CreateProjectsTable.php b/app/Database/Migrations/2025-01-01-000002_CreateProjectsTable.php new file mode 100644 index 0000000..736b8e6 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000002_CreateProjectsTable.php @@ -0,0 +1,52 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'color' => [ + 'type' => 'VARCHAR', + 'constraint' => 7, + 'null' => true, + 'comment' => 'Hex color for UI', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('projects'); + } + + public function down() + { + $this->forge->dropTable('projects'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000003_CreateTodosTable.php b/app/Database/Migrations/2025-01-01-000003_CreateTodosTable.php new file mode 100644 index 0000000..4d74547 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000003_CreateTodosTable.php @@ -0,0 +1,83 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'status' => [ + 'type' => 'ENUM', + 'constraint' => ['open', 'in_progress', 'completed', 'archived'], + 'default' => 'open', + ], + 'due_date' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'due_time' => [ + 'type' => 'TIME', + 'null' => true, + ], + 'sync_enabled' => [ + 'type' => 'BOOLEAN', + 'default' => true, + ], + 'reminder_enabled' => [ + 'type' => 'BOOLEAN', + 'default' => false, + ], + 'recurring_enabled' => [ + 'type' => 'BOOLEAN', + 'default' => false, + ], + 'project_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addKey('due_date'); + $this->forge->addKey('status'); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('project_id', 'projects', 'id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('todos'); + } + + public function down() + { + $this->forge->dropTable('todos'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000004_CreateTodoCategoriesTable.php b/app/Database/Migrations/2025-01-01-000004_CreateTodoCategoriesTable.php new file mode 100644 index 0000000..99472ac --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000004_CreateTodoCategoriesTable.php @@ -0,0 +1,33 @@ +forge->addField([ + 'todo_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'category_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + ]); + $this->forge->addPrimaryKey(['todo_id', 'category_id']); + $this->forge->addForeignKey('todo_id', 'todos', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('category_id', 'categories', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('todo_categories'); + } + + public function down() + { + $this->forge->dropTable('todo_categories'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000005_CreateRecurringTasksTable.php b/app/Database/Migrations/2025-01-01-000005_CreateRecurringTasksTable.php new file mode 100644 index 0000000..378e5d9 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000005_CreateRecurringTasksTable.php @@ -0,0 +1,64 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'schedule' => [ + 'type' => 'ENUM', + 'constraint' => ['daily', 'weekly', 'monthly', 'custom'], + 'null' => false, + ], + 'custom_days' => [ + 'type' => 'JSON', + 'null' => true, + 'comment' => 'Array of days e.g., ["mon","wed","fri"] when schedule=custom', + ], + 'favorite' => [ + 'type' => 'BOOLEAN', + 'default' => false, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('recurring_tasks'); + } + + public function down() + { + $this->forge->dropTable('recurring_tasks'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000006_CreateRecurringTaskCategoriesTable.php b/app/Database/Migrations/2025-01-01-000006_CreateRecurringTaskCategoriesTable.php new file mode 100644 index 0000000..1c256fb --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000006_CreateRecurringTaskCategoriesTable.php @@ -0,0 +1,33 @@ +forge->addField([ + 'recurring_task_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'category_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + ]); + $this->forge->addPrimaryKey(['recurring_task_id', 'category_id']); + $this->forge->addForeignKey('recurring_task_id', 'recurring_tasks', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('category_id', 'categories', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('recurring_task_categories'); + } + + public function down() + { + $this->forge->dropTable('recurring_task_categories'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000007_CreateActivityLogsTable.php b/app/Database/Migrations/2025-01-01-000007_CreateActivityLogsTable.php new file mode 100644 index 0000000..76f4b94 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000007_CreateActivityLogsTable.php @@ -0,0 +1,73 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => true, + 'comment' => 'Nullable for anonymous events', + ], + 'action' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + 'comment' => 'e.g., todo_created, login, theme_installed', + ], + 'entity_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + 'comment' => 'todo, category, project, etc.', + ], + 'entity_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => true, + ], + 'details' => [ + 'type' => 'JSON', + 'null' => true, + 'default' => '{}', + 'comment' => 'before/after values, metadata', + ], + 'ip_address' => [ + 'type' => 'VARCHAR', + 'constraint' => 45, + 'null' => true, + ], + 'user_agent' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addKey('created_at'); + $this->forge->addKey('action'); + $this->forge->addKey(['entity_type', 'entity_id']); + $this->forge->addForeignKey('user_id', 'users', 'id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('activity_logs'); + } + + public function down() + { + $this->forge->dropTable('activity_logs'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000008_CreateMarketplaceThemesTable.php b/app/Database/Migrations/2025-01-01-000008_CreateMarketplaceThemesTable.php new file mode 100644 index 0000000..26e1e76 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000008_CreateMarketplaceThemesTable.php @@ -0,0 +1,82 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'display_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'author' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + ], + 'version' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + ], + 'thumbnail_url' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'download_url' => [ + 'type' => 'TEXT', + 'null' => false, + ], + 'price' => [ + 'type' => 'DECIMAL', + 'constraint' => '10,2', + 'default' => 0, + ], + 'is_published' => [ + 'type' => 'BOOLEAN', + 'default' => true, + ], + 'metadata' => [ + 'type' => 'JSON', + 'null' => true, + 'default' => '{}', + 'comment' => 'tags, screenshots, etc.', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('name'); + $this->forge->createTable('marketplace_themes'); + } + + public function down() + { + $this->forge->dropTable('marketplace_themes'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000009_CreateUserThemesTable.php b/app/Database/Migrations/2025-01-01-000009_CreateUserThemesTable.php new file mode 100644 index 0000000..d87501c --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000009_CreateUserThemesTable.php @@ -0,0 +1,54 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'theme_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'installed_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'active' => [ + 'type' => 'BOOLEAN', + 'default' => false, + 'comment' => 'Whether this is the user\'s currently active theme', + ], + 'custom_settings' => [ + 'type' => 'JSON', + 'null' => true, + 'default' => '{}', + 'comment' => 'User overrides for theme variables', + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['user_id', 'theme_id']); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('theme_id', 'marketplace_themes', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('user_themes'); + } + + public function down() + { + $this->forge->dropTable('user_themes'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000010_CreateAiProvidersTable.php b/app/Database/Migrations/2025-01-01-000010_CreateAiProvidersTable.php new file mode 100644 index 0000000..04cb77c --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000010_CreateAiProvidersTable.php @@ -0,0 +1,52 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => false, + 'comment' => 'openai, anthropic, google, etc.', + ], + 'display_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'base_url' => [ + 'type' => 'TEXT', + 'null' => true, + 'comment' => 'Override endpoint', + ], + 'is_builtin' => [ + 'type' => 'BOOLEAN', + 'default' => true, + 'comment' => 'False for user-added custom providers', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('name'); + $this->forge->createTable('ai_providers'); + } + + public function down() + { + $this->forge->dropTable('ai_providers'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000011_CreateUserApiKeysTable.php b/app/Database/Migrations/2025-01-01-000011_CreateUserApiKeysTable.php new file mode 100644 index 0000000..6d44db5 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000011_CreateUserApiKeysTable.php @@ -0,0 +1,62 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'provider_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'api_key_encrypted' => [ + 'type' => 'TEXT', + 'null' => false, + 'comment' => 'Store encrypted API key', + ], + 'label' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + 'comment' => 'e.g., Work OpenAI Key', + ], + 'is_active' => [ + 'type' => 'BOOLEAN', + 'default' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'last_used_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['user_id', 'provider_id']); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('provider_id', 'ai_providers', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('user_api_keys'); + } + + public function down() + { + $this->forge->dropTable('user_api_keys'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000012_CreateUserAiSettingsTable.php b/app/Database/Migrations/2025-01-01-000012_CreateUserAiSettingsTable.php new file mode 100644 index 0000000..548e382 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000012_CreateUserAiSettingsTable.php @@ -0,0 +1,52 @@ +forge->addField([ + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'default_provider_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => true, + ], + 'default_model' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + 'comment' => 'e.g., gpt-4, claude-3-opus', + ], + 'max_tokens' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 2048, + ], + 'temperature' => [ + 'type' => 'FLOAT', + 'default' => 0.7, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addPrimaryKey('user_id'); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('default_provider_id', 'ai_providers', 'id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('user_ai_settings'); + } + + public function down() + { + $this->forge->dropTable('user_ai_settings'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000013_CreateAiChatsTable.php b/app/Database/Migrations/2025-01-01-000013_CreateAiChatsTable.php new file mode 100644 index 0000000..d4b6efb --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000013_CreateAiChatsTable.php @@ -0,0 +1,65 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'user_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => true, + 'comment' => 'Generated from first message or user-set', + ], + 'provider_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => true, + ], + 'model_used' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + 'null' => true, + 'comment' => 'Snapshot of model at chat creation', + ], + 'system_prompt' => [ + 'type' => 'TEXT', + 'null' => true, + 'comment' => 'Optional custom system prompt for this chat', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('user_id'); + $this->forge->addKey('updated_at'); + $this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('provider_id', 'ai_providers', 'id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('ai_chats'); + } + + public function down() + { + $this->forge->dropTable('ai_chats'); + } +} diff --git a/app/Database/Migrations/2025-01-01-000014_CreateAiMessagesTable.php b/app/Database/Migrations/2025-01-01-000014_CreateAiMessagesTable.php new file mode 100644 index 0000000..2582532 --- /dev/null +++ b/app/Database/Migrations/2025-01-01-000014_CreateAiMessagesTable.php @@ -0,0 +1,52 @@ +forge->addField([ + 'id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'chat_id' => [ + 'type' => 'CHAR', + 'constraint' => 36, + 'null' => false, + ], + 'role' => [ + 'type' => 'ENUM', + 'constraint' => ['user', 'assistant', 'system'], + 'null' => false, + ], + 'content' => [ + 'type' => 'TEXT', + 'null' => false, + ], + 'tokens_used' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => true, + 'comment' => 'Optional token count for billing/analysis', + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addKey('chat_id'); + $this->forge->addForeignKey('chat_id', 'ai_chats', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('ai_messages'); + } + + public function down() + { + $this->forge->dropTable('ai_messages'); + } +} diff --git a/app/Database/Seeds/AiProvidersSeeder.php b/app/Database/Seeds/AiProvidersSeeder.php new file mode 100644 index 0000000..eb64ac2 --- /dev/null +++ b/app/Database/Seeds/AiProvidersSeeder.php @@ -0,0 +1,40 @@ + '550e8400-e29b-41d4-a716-446655440001', + 'name' => 'openai', + 'display_name' => 'OpenAI', + 'base_url' => 'https://api.openai.com/v1', + 'is_builtin' => true, + 'created_at' => date('Y-m-d H:i:s'), + ], + [ + 'id' => '550e8400-e29b-41d4-a716-446655440002', + 'name' => 'anthropic', + 'display_name' => 'Anthropic', + 'base_url' => 'https://api.anthropic.com', + 'is_builtin' => true, + 'created_at' => date('Y-m-d H:i:s'), + ], + [ + 'id' => '550e8400-e29b-41d4-a716-446655440003', + 'name' => 'google', + 'display_name' => 'Google AI', + 'base_url' => 'https://generativelanguage.googleapis.com/v1', + 'is_builtin' => true, + 'created_at' => date('Y-m-d H:i:s'), + ], + ]; + + $this->db->table('ai_providers')->insertBatch($data); + } +} diff --git a/app/Database/Seeds/MarketplaceThemesSeeder.php b/app/Database/Seeds/MarketplaceThemesSeeder.php new file mode 100644 index 0000000..9d26bc3 --- /dev/null +++ b/app/Database/Seeds/MarketplaceThemesSeeder.php @@ -0,0 +1,46 @@ + '550e8400-e29b-41d4-a716-446655440010', + 'name' => 'default-light', + 'display_name' => 'Default Light', + 'description' => 'Clean and simple light theme', + 'author' => 'System', + 'version' => '1.0.0', + 'thumbnail_url' => null, + 'download_url' => '/themes/default-light.zip', + 'price' => 0, + 'is_published' => true, + 'metadata' => json_encode(['tags' => ['light', 'clean']]), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ], + [ + 'id' => '550e8400-e29b-41d4-a716-446655440011', + 'name' => 'default-dark', + 'display_name' => 'Default Dark', + 'description' => 'Dark theme for night owls', + 'author' => 'System', + 'version' => '1.0.0', + 'thumbnail_url' => null, + 'download_url' => '/themes/default-dark.zip', + 'price' => 0, + 'is_published' => true, + 'metadata' => json_encode(['tags' => ['dark', 'night']]), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ], + ]; + + $this->db->table('marketplace_themes')->insertBatch($data); + } +} diff --git a/app/Database/Seeds/SampleDataSeeder.php b/app/Database/Seeds/SampleDataSeeder.php new file mode 100644 index 0000000..ec9aef2 --- /dev/null +++ b/app/Database/Seeds/SampleDataSeeder.php @@ -0,0 +1,313 @@ +db->table('users')->where('email', 'demo@example.com')->get()->getRowArray(); + if ($existingUser) { + $userId = $existingUser['id']; + } else { + $userId = $generateUuid(); + $this->db->table('users')->insert([ + 'id' => $userId, + 'email' => 'demo@example.com', + 'password_hash' => password_hash('password123', PASSWORD_DEFAULT), + 'name' => 'Demo User', + 'avatar_url' => null, + 'settings' => json_encode(['language' => 'en', 'default_view' => 'list']), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + + // Create sample categories (check for existing) + $existingCategories = $this->db->table('categories')->where('user_id', $userId)->get()->getResultArray(); + $existingCategoryNames = array_column($existingCategories, 'name'); + + $categories = []; + $categoryNames = ['Work', 'Home', 'Personal']; + $categoryColors = ['#3B82F6', '#10B981', '#F59E0B']; + $categoryFavorites = [true, false, false]; + + foreach ($categoryNames as $index => $name) { + if (!in_array($name, $existingCategoryNames)) { + $categories[] = [ + 'id' => $generateUuid(), + 'user_id' => $userId, + 'name' => $name, + 'color' => $categoryColors[$index], + 'favorite' => $categoryFavorites[$index], + 'created_at' => date('Y-m-d H:i:s'), + ]; + } + } + + if (!empty($categories)) { + $this->db->table('categories')->insertBatch($categories); + } + + // Get all categories for the user (existing + newly created) + $allCategories = $this->db->table('categories')->where('user_id', $userId)->get()->getResultArray(); + $categories = []; + foreach ($categoryNames as $name) { + foreach ($allCategories as $cat) { + if ($cat['name'] === $name) { + $categories[] = $cat; + break; + } + } + } + + // Create sample projects (check for existing) + $existingProjects = $this->db->table('projects')->where('user_id', $userId)->get()->getResultArray(); + $existingProjectNames = array_column($existingProjects, 'name'); + + $projects = []; + $projectData = [ + ['name' => 'Web Redesign', 'description' => 'Redesign the company website', 'color' => '#8B5CF6'], + ['name' => 'Home Renovation', 'description' => 'Renovate the kitchen and bathroom', 'color' => '#EC4899'], + ['name' => 'Learning', 'description' => 'Learn new technologies and skills', 'color' => '#14B8A6'], + ]; + + foreach ($projectData as $proj) { + if (!in_array($proj['name'], $existingProjectNames)) { + $projects[] = [ + 'id' => $generateUuid(), + 'user_id' => $userId, + 'name' => $proj['name'], + 'description' => $proj['description'], + 'color' => $proj['color'], + 'created_at' => date('Y-m-d H:i:s'), + ]; + } + } + + if (!empty($projects)) { + $this->db->table('projects')->insertBatch($projects); + } + + // Get all projects for the user + $allProjects = $this->db->table('projects')->where('user_id', $userId)->get()->getResultArray(); + $projects = []; + foreach ($projectData as $proj) { + foreach ($allProjects as $p) { + if ($p['name'] === $proj['name']) { + $projects[] = $p; + break; + } + } + } + + $webRedesignId = isset($projects[0]) ? $projects[0]['id'] : null; + $homeRenovationId = isset($projects[1]) ? $projects[1]['id'] : null; + $learningId = isset($projects[2]) ? $projects[2]['id'] : null; + + // Create sample todos (check for existing) + $existingTodos = $this->db->table('todos')->where('user_id', $userId)->get()->getResultArray(); + $existingTodoTitles = array_column($existingTodos, 'title'); + + $todos = []; + $todoData = [ + [ + 'title' => 'Bestehende Aufgaben analysieren', + 'description' => 'Aktuellen Aufbau der Todo-App sichten und Felder abstimmen.', + 'status' => 'open', + 'due_date' => date('Y-m-d', strtotime('+7 days')), + 'due_time' => '10:30:00', + 'sync_enabled' => true, + 'reminder_enabled' => false, + 'recurring_enabled' => false, + 'project_id' => $webRedesignId, + ], + [ + 'title' => 'Wireframes erstellen', + 'description' => 'Erste Skizzen für das neue Design machen.', + 'status' => 'in_progress', + 'due_date' => date('Y-m-d', strtotime('+14 days')), + 'sync_enabled' => true, + 'reminder_enabled' => true, + 'recurring_enabled' => false, + 'project_id' => $webRedesignId, + ], + [ + 'title' => 'Küche planen', + 'description' => 'Neue Küche auswählen und bestellen.', + 'status' => 'open', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'sync_enabled' => false, + 'reminder_enabled' => true, + 'recurring_enabled' => false, + 'project_id' => $homeRenovationId, + ], + [ + 'title' => 'CodeIgniter lernen', + 'description' => 'Offizielle Dokumentation durchgehen.', + 'status' => 'completed', + 'due_date' => date('Y-m-d', strtotime('-5 days')), + 'sync_enabled' => true, + 'reminder_enabled' => false, + 'recurring_enabled' => false, + 'project_id' => $learningId, + ], + [ + 'title' => 'Einkaufen', + 'description' => 'Milch, Brot, Eier, Gemüse', + 'status' => 'open', + 'due_date' => date('Y-m-d', strtotime('+1 day')), + 'sync_enabled' => true, + 'reminder_enabled' => true, + 'recurring_enabled' => false, + 'project_id' => null, + ], + ]; + + foreach ($todoData as $todo) { + if (!in_array($todo['title'], $existingTodoTitles)) { + $todos[] = array_merge($todo, [ + 'id' => $generateUuid(), + 'user_id' => $userId, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + } + + if (!empty($todos)) { + $this->db->table('todos')->insertBatch($todos); + } + + // Get all todos for the user + $allTodos = $this->db->table('todos')->where('user_id', $userId)->get()->getResultArray(); + + // Link todos to categories + $workCategoryId = $categories[0]['id']; + $homeCategoryId = $categories[1]['id']; + $personalCategoryId = $categories[2]['id']; + + $todoCategories = []; + $todoCategoryMap = [ + 'Bestehende Aufgaben analysieren' => $workCategoryId, + 'Wireframes erstellen' => $workCategoryId, + 'Küche planen' => $homeCategoryId, + 'CodeIgniter lernen' => $workCategoryId, + 'Einkaufen' => $personalCategoryId, + ]; + + foreach ($allTodos as $todo) { + if (isset($todoCategoryMap[$todo['title']])) { + // Check if this link already exists + $existingLink = $this->db->table('todo_categories') + ->where('todo_id', $todo['id']) + ->where('category_id', $todoCategoryMap[$todo['title']]) + ->get() + ->getRowArray(); + + if (!$existingLink) { + $todoCategories[] = [ + 'todo_id' => $todo['id'], + 'category_id' => $todoCategoryMap[$todo['title']], + ]; + } + } + } + + if (!empty($todoCategories)) { + $this->db->table('todo_categories')->insertBatch($todoCategories); + } + + // Create sample recurring tasks (check for existing) + $existingRecurringTasks = $this->db->table('recurring_tasks')->where('user_id', $userId)->get()->getResultArray(); + $existingRecurringTaskTitles = array_column($existingRecurringTasks, 'title'); + + $recurringTasks = []; + $recurringTaskData = [ + [ + 'title' => 'Weekly review', + 'description' => 'Plan next week\'s tasks', + 'schedule' => 'weekly', + 'custom_days' => json_encode([]), + 'favorite' => true, + ], + [ + 'title' => 'Clean the house', + 'description' => 'Every Saturday', + 'schedule' => 'custom', + 'custom_days' => json_encode(['sat']), + 'favorite' => false, + ], + [ + 'title' => 'Daily standup', + 'description' => 'Team meeting every morning', + 'schedule' => 'daily', + 'custom_days' => json_encode([]), + 'favorite' => true, + ], + ]; + + foreach ($recurringTaskData as $task) { + if (!in_array($task['title'], $existingRecurringTaskTitles)) { + $recurringTasks[] = array_merge($task, [ + 'id' => $generateUuid(), + 'user_id' => $userId, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + } + + if (!empty($recurringTasks)) { + $this->db->table('recurring_tasks')->insertBatch($recurringTasks); + } + + // Get all recurring tasks for the user + $allRecurringTasks = $this->db->table('recurring_tasks')->where('user_id', $userId)->get()->getResultArray(); + + // Link recurring tasks to categories + $recurringTaskCategories = []; + $recurringTaskCategoryMap = [ + 'Weekly review' => $workCategoryId, + 'Clean the house' => $homeCategoryId, + 'Daily standup' => $workCategoryId, + ]; + + foreach ($allRecurringTasks as $task) { + if (isset($recurringTaskCategoryMap[$task['title']])) { + // Check if this link already exists + $existingLink = $this->db->table('recurring_task_categories') + ->where('recurring_task_id', $task['id']) + ->where('category_id', $recurringTaskCategoryMap[$task['title']]) + ->get() + ->getRowArray(); + + if (!$existingLink) { + $recurringTaskCategories[] = [ + 'recurring_task_id' => $task['id'], + 'category_id' => $recurringTaskCategoryMap[$task['title']], + ]; + } + } + } + + if (!empty($recurringTaskCategories)) { + $this->db->table('recurring_task_categories')->insertBatch($recurringTaskCategories); + } + } +} diff --git a/app/Models/ActivityLogModel.php b/app/Models/ActivityLogModel.php new file mode 100644 index 0000000..1b9bc09 --- /dev/null +++ b/app/Models/ActivityLogModel.php @@ -0,0 +1,97 @@ + 'required|max_length[255]', + ]; + + // Log an activity + public function logActivity($data) + { + // Disable events to prevent any recursive logging + $this->skipEvents(); + + if (!isset($data['id'])) { + $data['id'] = $this->generateUuid(); + } + if (!isset($data['created_at'])) { + $data['created_at'] = date('Y-m-d H:i:s'); + } + + $result = $this->insert($data); + + // Re-enable events + $this->skipEvents(false); + + return $result; + } + + // Get logs by user + public function getByUser($userId, $limit = 50) + { + return $this->where('user_id', $userId) + ->orderBy('created_at', 'DESC') + ->limit($limit) + ->get() + ->getResultArray(); + } + + // Get logs by entity + public function getByEntity($entityType, $entityId, $limit = 50) + { + return $this->where('entity_type', $entityType) + ->where('entity_id', $entityId) + ->orderBy('created_at', 'DESC') + ->limit($limit) + ->get() + ->getResultArray(); + } + + // Get logs by action + public function getByAction($action, $limit = 50) + { + return $this->where('action', $action) + ->orderBy('created_at', 'DESC') + ->limit($limit) + ->get() + ->getResultArray(); + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/app/Models/AiChatModel.php b/app/Models/AiChatModel.php new file mode 100644 index 0000000..143a612 --- /dev/null +++ b/app/Models/AiChatModel.php @@ -0,0 +1,105 @@ + 'required', + ]; + + // Get chats by user + public function getByUser($userId, $limit = 50) + { + return $this->where('user_id', $userId) + ->orderBy('updated_at', 'DESC') + ->limit($limit) + ->get() + ->getResultArray(); + } + + // Get chat with message count + public function getWithMessageCount($chatId) + { + $chat = $this->find($chatId); + if ($chat) { + $messageModel = new AiMessageModel(); + $chat['message_count'] = $messageModel->where('chat_id', $chatId)->countAllResults(); + } + return $chat; + } + + // Get all chats by user with message counts + public function getByUserWithMessageCounts($userId) + { + $chats = $this->getByUser($userId); + $messageModel = new AiMessageModel(); + + foreach ($chats as &$chat) { + $chat['message_count'] = $messageModel->where('chat_id', $chat['id'])->countAllResults(); + } + + return $chats; + } + + // Get chat with provider info + public function getWithProvider($chatId) + { + return $this->select('ai_chats.*, ai_providers.name as provider_name, ai_providers.display_name') + ->join('ai_providers', 'ai_chats.provider_id = ai_providers.id', 'left') + ->where('ai_chats.id', $chatId) + ->get() + ->getRowArray(); + } + + // Update chat title + public function updateTitle($chatId, $title) + { + return $this->update($chatId, ['title' => $title]); + } + + // Create new chat + public function createChat($userId, $data = []) + { + $data['id'] = $this->generateUuid(); + $data['user_id'] = $userId; + $data['created_at'] = date('Y-m-d H:i:s'); + $data['updated_at'] = date('Y-m-d H:i:s'); + + return $this->insert($data); + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/app/Models/AiMessageModel.php b/app/Models/AiMessageModel.php new file mode 100644 index 0000000..1871407 --- /dev/null +++ b/app/Models/AiMessageModel.php @@ -0,0 +1,93 @@ + 'required', + 'role' => 'required|in_list[user,assistant,system]', + 'content' => 'required', + ]; + + // Get messages by chat + public function getByChat($chatId) + { + return $this->where('chat_id', $chatId) + ->orderBy('created_at', 'ASC') + ->get() + ->getResultArray(); + } + + // Add message to chat + public function addMessage($chatId, $role, $content, $tokensUsed = null) + { + return $this->insert([ + 'id' => $this->generateUuid(), + 'chat_id' => $chatId, + 'role' => $role, + 'content' => $content, + 'tokens_used' => $tokensUsed, + 'created_at' => date('Y-m-d H:i:s'), + ]); + } + + // Get last message from chat + public function getLastMessage($chatId) + { + return $this->where('chat_id', $chatId) + ->orderBy('created_at', 'DESC') + ->limit(1) + ->get() + ->getRowArray(); + } + + // Delete all messages from chat + public function deleteByChat($chatId) + { + return $this->where('chat_id', $chatId)->delete(); + } + + // Get total tokens used by chat + public function getTotalTokens($chatId) + { + $result = $this->selectSum('tokens_used') + ->where('chat_id', $chatId) + ->get() + ->getRowArray(); + + return $result ? (int) $result['tokens_used'] : 0; + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/app/Models/AiProviderModel.php b/app/Models/AiProviderModel.php new file mode 100644 index 0000000..ce0a587 --- /dev/null +++ b/app/Models/AiProviderModel.php @@ -0,0 +1,57 @@ + 'required|max_length[100]|is_unique[ai_providers.name]', + 'display_name' => 'required|max_length[255]', + ]; + + // Get builtin providers only + public function getBuiltinProviders() + { + return $this->where('is_builtin', true) + ->orderBy('name', 'ASC') + ->get() + ->getResultArray(); + } + + // Get custom providers only + public function getCustomProviders() + { + return $this->where('is_builtin', false) + ->orderBy('name', 'ASC') + ->get() + ->getResultArray(); + } + + // Get provider by name + public function getByName($name) + { + return $this->where('name', $name) + ->get() + ->getRowArray(); + } +} diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php new file mode 100644 index 0000000..a775a0f --- /dev/null +++ b/app/Models/CategoryModel.php @@ -0,0 +1,38 @@ + 'required', + 'name' => 'required|max_length[255]', + ]; + + protected function getEntityType(): string + { + return 'category'; + } +} diff --git a/app/Models/LoggableTrait.php b/app/Models/LoggableTrait.php new file mode 100644 index 0000000..822ff6b --- /dev/null +++ b/app/Models/LoggableTrait.php @@ -0,0 +1,142 @@ +logActivity('created', $data); + } catch (\Exception $e) { + // Silently fail to avoid breaking the main operation + log_message('error', 'Failed to log activity: ' . $e->getMessage()); + } + return $data; + } + + /** + * Log activity after update + */ + protected function afterUpdate(array $data) + { + try { + $this->logActivity('updated', $data); + } catch (\Exception $e) { + log_message('error', 'Failed to log activity: ' . $e->getMessage()); + } + return $data; + } + + /** + * Log activity after delete + */ + protected function afterDelete(array $data) + { + try { + $this->logActivity('deleted', $data); + } catch (\Exception $e) { + log_message('error', 'Failed to log activity: ' . $e->getMessage()); + } + return $data; + } + + /** + * Log activity to activity_logs table + */ + protected function logActivity($action, $data) + { + $activityLogModel = new ActivityLogModel(); + + $entityType = $this->getEntityType(); + $entityId = $data['id'] ?? $data[$this->primaryKey] ?? null; + $userId = $data['user_id'] ?? null; + + // Try to get user from session if not in data + if ($userId === null && function_exists('session')) { + $userId = session()->get('user_id'); + } + + $logData = [ + 'user_id' => $userId, + 'action' => $this->getActionName($action, $entityType), + 'entity_type' => $entityType, + 'entity_id' => $entityId, + 'details' => json_encode($this->getLogDetails($action, $data)), + 'ip_address' => $this->getClientIp(), + 'user_agent' => $this->getUserAgent(), + ]; + + $activityLogModel->logActivity($logData); + } + + /** + * Get entity type based on table name + */ + protected function getEntityType(): string + { + $table = $this->table; + // Remove plural 's' if present + return rtrim($table, 's'); + } + + /** + * Get formatted action name + */ + protected function getActionName($action, $entityType): string + { + return "{$entityType}_{$action}"; + } + + /** + * Get log details (can be overridden in models) + */ + protected function getLogDetails($action, $data): array + { + $details = [ + 'action' => $action, + ]; + + // Add relevant fields based on entity type + if (isset($data['title'])) { + $details['title'] = $data['title']; + } + if (isset($data['name'])) { + $details['name'] = $data['name']; + } + if (isset($data['email'])) { + $details['email'] = $data['email']; + } + + return $details; + } + + /** + * Get client IP address + */ + protected function getClientIp(): ?string + { + try { + $request = \Config\Services::request(); + return $request->getIPAddress(); + } catch (\Exception $e) { + return 'CLI'; + } + } + + /** + * Get user agent + */ + protected function getUserAgent(): ?string + { + try { + $request = \Config\Services::request(); + return $request->getUserAgent()->toString(); + } catch (\Exception $e) { + return 'CLI/Script'; + } + } +} diff --git a/app/Models/MarketplaceThemeModel.php b/app/Models/MarketplaceThemeModel.php new file mode 100644 index 0000000..453660b --- /dev/null +++ b/app/Models/MarketplaceThemeModel.php @@ -0,0 +1,68 @@ + 'required|max_length[255]|is_unique[marketplace_themes.name]', + 'display_name' => 'required|max_length[255]', + 'download_url' => 'required', + ]; + + // Get published themes only + public function getPublished() + { + return $this->where('is_published', true) + ->orderBy('created_at', 'DESC') + ->get() + ->getResultArray(); + } + + // Get free themes + public function getFreeThemes() + { + return $this->where('price', 0) + ->where('is_published', true) + ->orderBy('created_at', 'DESC') + ->get() + ->getResultArray(); + } + + // Get paid themes + public function getPaidThemes() + { + return $this->where('price >', 0) + ->where('is_published', true) + ->orderBy('price', 'ASC') + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/ProjectModel.php b/app/Models/ProjectModel.php new file mode 100644 index 0000000..c5691f7 --- /dev/null +++ b/app/Models/ProjectModel.php @@ -0,0 +1,38 @@ + 'required', + 'name' => 'required|max_length[255]', + ]; + + protected function getEntityType(): string + { + return 'project'; + } +} diff --git a/app/Models/RecurringTaskCategoryModel.php b/app/Models/RecurringTaskCategoryModel.php new file mode 100644 index 0000000..0dcf094 --- /dev/null +++ b/app/Models/RecurringTaskCategoryModel.php @@ -0,0 +1,57 @@ +insert([ + 'recurring_task_id' => $taskId, + 'category_id' => $categoryId, + ]); + } + + // Remove category from recurring task + public function removeCategoryFromTask($taskId, $categoryId) + { + return $this->where('recurring_task_id', $taskId) + ->where('category_id', $categoryId) + ->delete(); + } + + // Get categories for a recurring task + public function getCategoriesForTask($taskId) + { + return $this->select('categories.*') + ->join('categories', 'recurring_task_categories.category_id = categories.id') + ->where('recurring_task_categories.recurring_task_id', $taskId) + ->get() + ->getResultArray(); + } + + // Get recurring tasks for a category + public function getTasksForCategory($categoryId) + { + return $this->select('recurring_tasks.*') + ->join('recurring_tasks', 'recurring_task_categories.recurring_task_id = recurring_tasks.id') + ->where('recurring_task_categories.category_id', $categoryId) + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/RecurringTaskModel.php b/app/Models/RecurringTaskModel.php new file mode 100644 index 0000000..945073d --- /dev/null +++ b/app/Models/RecurringTaskModel.php @@ -0,0 +1,69 @@ + 'required', + 'title' => 'required|max_length[255]', + 'schedule' => 'required|in_list[daily,weekly,monthly,custom]', + ]; + + protected function getEntityType(): string + { + return 'recurring_task'; + } + + // Get recurring tasks with categories + public function getWithCategories($taskId = null) + { + $builder = $this->select('recurring_tasks.*, GROUP_CONCAT(categories.name) as category_names') + ->join('recurring_task_categories', 'recurring_tasks.id = recurring_task_categories.recurring_task_id', 'left') + ->join('categories', 'recurring_task_categories.category_id = categories.id', 'left') + ->groupBy('recurring_tasks.id'); + + if ($taskId) { + $builder->where('recurring_tasks.id', $taskId); + } + + return $builder->get()->getResultArray(); + } + + // Get recurring tasks by user with categories + public function getByUserWithCategories($userId) + { + return $this->select('recurring_tasks.*, GROUP_CONCAT(categories.name) as category_names') + ->join('recurring_task_categories', 'recurring_tasks.id = recurring_task_categories.recurring_task_id', 'left') + ->join('categories', 'recurring_task_categories.category_id = categories.id', 'left') + ->where('recurring_tasks.user_id', $userId) + ->groupBy('recurring_tasks.id') + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/TodoCategoryModel.php b/app/Models/TodoCategoryModel.php new file mode 100644 index 0000000..fc4f56e --- /dev/null +++ b/app/Models/TodoCategoryModel.php @@ -0,0 +1,57 @@ +insert([ + 'todo_id' => $todoId, + 'category_id' => $categoryId, + ]); + } + + // Remove category from todo + public function removeCategoryFromTodo($todoId, $categoryId) + { + return $this->where('todo_id', $todoId) + ->where('category_id', $categoryId) + ->delete(); + } + + // Get categories for a todo + public function getCategoriesForTodo($todoId) + { + return $this->select('categories.*') + ->join('categories', 'todo_categories.category_id = categories.id') + ->where('todo_categories.todo_id', $todoId) + ->get() + ->getResultArray(); + } + + // Get todos for a category + public function getTodosForCategory($categoryId) + { + return $this->select('todos.*') + ->join('todos', 'todo_categories.todo_id = todos.id') + ->where('todo_categories.category_id', $categoryId) + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/TodoModel.php b/app/Models/TodoModel.php new file mode 100644 index 0000000..3a484f4 --- /dev/null +++ b/app/Models/TodoModel.php @@ -0,0 +1,73 @@ + 'required', + 'title' => 'required|max_length[255]', + 'status' => 'permit_empty|in_list[open,in_progress,completed,archived]', + ]; + + protected function getEntityType(): string + { + return 'todo'; + } + + // Get todos with categories + public function getWithCategories($todoId = null) + { + $builder = $this->select('todos.*, GROUP_CONCAT(categories.name) as category_names') + ->join('todo_categories', 'todos.id = todo_categories.todo_id', 'left') + ->join('categories', 'todo_categories.category_id = categories.id', 'left') + ->groupBy('todos.id'); + + if ($todoId) { + $builder->where('todos.id', $todoId); + } + + return $builder->get()->getResultArray(); + } + + // Get todos by user with categories + public function getByUserWithCategories($userId) + { + return $this->select('todos.*, GROUP_CONCAT(categories.name) as category_names') + ->join('todo_categories', 'todos.id = todo_categories.todo_id', 'left') + ->join('categories', 'todo_categories.category_id = categories.id', 'left') + ->where('todos.user_id', $userId) + ->groupBy('todos.id') + ->get() + ->getResultArray(); + } +} diff --git a/app/Models/UserAiSettingsModel.php b/app/Models/UserAiSettingsModel.php new file mode 100644 index 0000000..8bca40d --- /dev/null +++ b/app/Models/UserAiSettingsModel.php @@ -0,0 +1,68 @@ + 'required', + 'max_tokens' => 'permit_empty|integer|greater_than[0]', + 'temperature' => 'permit_empty|numeric|greater_than_equal_to[0]|less_than_equal_to[2]', + ]; + + // Get or create settings for user + public function getSettings($userId) + { + $settings = $this->find($userId); + + if (!$settings) { + // Create default settings + $this->insert([ + 'user_id' => $userId, + 'default_provider_id' => null, + 'default_model' => null, + 'max_tokens' => 2048, + 'temperature' => 0.7, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + $settings = $this->find($userId); + } + + return $settings; + } + + // Update settings for user + public function updateSettings($userId, $data) + { + $data['updated_at'] = date('Y-m-d H:i:s'); + return $this->update($userId, $data); + } + + // Get settings with provider info + public function getSettingsWithProvider($userId) + { + return $this->select('user_ai_settings.*, ai_providers.name as provider_name, ai_providers.display_name') + ->join('ai_providers', 'user_ai_settings.default_provider_id = ai_providers.id', 'left') + ->where('user_ai_settings.user_id', $userId) + ->get() + ->getRowArray(); + } +} diff --git a/app/Models/UserApiKeyModel.php b/app/Models/UserApiKeyModel.php new file mode 100644 index 0000000..d2b8fe1 --- /dev/null +++ b/app/Models/UserApiKeyModel.php @@ -0,0 +1,108 @@ + 'required', + 'provider_id' => 'required', + 'api_key_encrypted' => 'required', + ]; + + // Save or update API key for user and provider + public function saveApiKey($userId, $providerId, $encryptedKey, $label = null) + { + $existing = $this->where('user_id', $userId) + ->where('provider_id', $providerId) + ->first(); + + if ($existing) { + return $this->update($existing['id'], [ + 'api_key_encrypted' => $encryptedKey, + 'label' => $label, + 'is_active' => true, + 'last_used_at' => null, + ]); + } else { + return $this->insert([ + 'id' => $this->generateUuid(), + 'user_id' => $userId, + 'provider_id' => $providerId, + 'api_key_encrypted' => $encryptedKey, + 'label' => $label, + 'is_active' => true, + 'created_at' => date('Y-m-d H:i:s'), + 'last_used_at' => null, + ]); + } + } + + // Get API key for user and provider + public function getApiKey($userId, $providerId) + { + return $this->where('user_id', $userId) + ->where('provider_id', $providerId) + ->where('is_active', true) + ->first(); + } + + // Get all API keys for user + public function getUserApiKeys($userId) + { + return $this->select('user_api_keys.*, ai_providers.name as provider_name, ai_providers.display_name') + ->join('ai_providers', 'user_api_keys.provider_id = ai_providers.id') + ->where('user_api_keys.user_id', $userId) + ->orderBy('user_api_keys.created_at', 'DESC') + ->get() + ->getResultArray(); + } + + // Deactivate API key + public function deactivateApiKey($userId, $providerId) + { + return $this->where('user_id', $userId) + ->where('provider_id', $providerId) + ->update(['is_active' => false]); + } + + // Update last used timestamp + public function updateLastUsed($userId, $providerId) + { + return $this->where('user_id', $userId) + ->where('provider_id', $providerId) + ->update(['last_used_at' => date('Y-m-d H:i:s')]); + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php new file mode 100644 index 0000000..ef1145f --- /dev/null +++ b/app/Models/UserModel.php @@ -0,0 +1,48 @@ + 'required|valid_email|is_unique[users.email]', + 'password_hash' => 'required', + ]; + + protected $validationMessages = [ + 'email' => [ + 'required' => 'Email is required', + 'valid_email' => 'Please enter a valid email address', + 'is_unique' => 'This email is already registered', + ], + ]; + + protected function getEntityType(): string + { + return 'user'; + } +} diff --git a/app/Models/UserThemeModel.php b/app/Models/UserThemeModel.php new file mode 100644 index 0000000..6ee6047 --- /dev/null +++ b/app/Models/UserThemeModel.php @@ -0,0 +1,104 @@ + 'required', + 'theme_id' => 'required', + ]; + + // Install theme for user + public function installTheme($userId, $themeId) + { + return $this->insert([ + 'id' => $this->generateUuid(), + 'user_id' => $userId, + 'theme_id' => $themeId, + 'installed_at' => date('Y-m-d H:i:s'), + 'active' => false, + 'custom_settings' => json_encode([]), + ]); + } + + // Uninstall theme for user + public function uninstallTheme($userId, $themeId) + { + return $this->where('user_id', $userId) + ->where('theme_id', $themeId) + ->delete(); + } + + // Set active theme for user + public function setActiveTheme($userId, $themeId) + { + // Deactivate all themes for user + $this->where('user_id', $userId)->update(['active' => false]); + + // Activate the specified theme + return $this->where('user_id', $userId) + ->where('theme_id', $themeId) + ->update(['active' => true]); + } + + // Get active theme for user + public function getActiveTheme($userId) + { + return $this->select('user_themes.*, marketplace_themes.*') + ->join('marketplace_themes', 'user_themes.theme_id = marketplace_themes.id') + ->where('user_themes.user_id', $userId) + ->where('user_themes.active', true) + ->get() + ->getRowArray(); + } + + // Get all installed themes for user + public function getUserThemes($userId) + { + return $this->select('user_themes.*, marketplace_themes.*') + ->join('marketplace_themes', 'user_themes.theme_id = marketplace_themes.id') + ->where('user_themes.user_id', $userId) + ->orderBy('user_themes.installed_at', 'DESC') + ->get() + ->getResultArray(); + } + + // Check if theme is installed for user + public function isInstalled($userId, $themeId) + { + return $this->where('user_id', $userId) + ->where('theme_id', $themeId) + ->countAllResults() > 0; + } + + private function generateUuid() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} diff --git a/example.env b/example.env new file mode 100644 index 0000000..0cdb5b3 --- /dev/null +++ b/example.env @@ -0,0 +1,69 @@ +#-------------------------------------------------------------------- +# Example Environment Configuration file +# +# This file can be used as a starting point for your own +# custom .env files, and contains most of the possible settings +# available in a default install. +# +# By default, all of the settings are commented out. If you want +# to override the setting, you must un-comment it by removing the '#' +# at the beginning of the line. +#-------------------------------------------------------------------- + +#-------------------------------------------------------------------- +# ENVIRONMENT +#-------------------------------------------------------------------- + +CI_ENVIRONMENT = development + +#-------------------------------------------------------------------- +# APP +#-------------------------------------------------------------------- + +# app.baseURL = '' +# If you have trouble with `.`, you could also use `_`. +# app_baseURL = '' +# app.forceGlobalSecureRequests = false +# app.CSPEnabled = false + +#-------------------------------------------------------------------- +# DATABASE +#-------------------------------------------------------------------- + +database.default.hostname = 127.0.0.1 +database.default.database = TodoApp +database.default.username = root +database.default.password = +database.default.DBDriver = MySQLi +# database.default.DBPrefix = +database.default.port = 3306 + +# If you use MySQLi as tests, first update the values of Config\Database::$tests. +# database.tests.hostname = localhost +# database.tests.database = ci4_test +# database.tests.username = root +# database.tests.password = root +# database.tests.DBDriver = MySQLi +# database.tests.DBPrefix = +# database.tests.charset = utf8mb4 +# database.tests.DBCollat = utf8mb4_general_ci +# database.tests.port = 3306 + +#-------------------------------------------------------------------- +# ENCRYPTION +#-------------------------------------------------------------------- + +# encryption.key = + +#-------------------------------------------------------------------- +# SESSION +#-------------------------------------------------------------------- + +# session.driver = 'CodeIgniter\Session\Handlers\FileHandler' +# session.savePath = null + +#-------------------------------------------------------------------- +# LOGGER +#-------------------------------------------------------------------- + +# logger.threshold = 4