# AfterSales CRM - AI Agent Instructions > **Trước khi làm việc, đọc các file quan trọng:** > - `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (thay vì scan lại code) > - `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm > - `PROGRESS.md` — Lịch sử tiến độ hoàn thành ## Overview AfterSales CRM is a real-estate after-sales customer care system built with **Laravel 13.6** + **Filament 5.6**. ## How to Run ```bash cd /home/phuongtc/vibecode/minicrm php artisan migrate:fresh --seed php artisan serve # Open http://localhost:8000/admin ``` ### Test Accounts (seeded) | Role | Email | Password | |------|-------|----------| | Admin | admin@minicrm.local | password | | Manager | manager@minicrm.local | password | | Staff | staff@minicrm.local | password | ## Tech Stack Versions - PHP 8.3 - Laravel 13.6.0 (`vendor/laravel/framework`) - Filament 5.6.1 (`vendor/filament/filament`) - Database: SQLite (`database/database.sqlite`) - Composer: `/home/phuongtc/composer` - Spatie: laravel-permission (role-based auth) - Testing: Pest PHP ## Running Commands ```bash cd /home/phuongtc/vibecode/minicrm php artisan make:model Foo php artisan make:filament-resource Foo --generate php artisan make:filament-relation-manager ResourceName relationship titleAttribute php artisan make:migration create_foo_table php artisan migrate php artisan db:seed php artisan route:list php artisan storage:link # needed for file uploads php artisan test # run pest tests ``` ## Directory Structure ``` minicrm/ ├── app/ │ ├── Filament/ │ │ ├── Resources/ │ │ │ ├── Feedback/ │ │ │ │ ├── FeedbackResource.php │ │ │ │ ├── Schemas/FeedbackForm.php │ │ │ │ ├── Tables/FeedbackTable.php │ │ │ │ ├── Pages/{Create,Edit,List}Feedback.php │ │ │ │ └── RelationManagers/InteractionsRelationManager.php │ │ │ ├── Products/ │ │ │ ├── Customers/ │ │ │ │ └── RelationManagers/FeedbacksRelationManager.php │ │ │ ├── FeedbackChannels/ │ │ │ └── FeedbackTags/ │ │ └── Widgets/ │ │ ├── ResolvedTicketsWidget.php # Dashboard: resolved tickets table │ │ ├── AvgProcessingTimeWidget.php # Dashboard: stats overview │ │ └── HandlerPerformanceWidget.php # Dashboard: bar chart per handler │ ├── Models/ │ │ ├── User.php # HasRoles (Spatie), isAdmin(), isManager() │ │ ├── Department.php # NEW: name, manager_id → User │ │ ├── Product.php │ │ ├── Customer.php │ │ ├── CustomerProduct.php │ │ ├── Feedback.php # added: current_department_id, is_escalated, transferLogs │ │ ├── FeedbackChannel.php │ │ ├── FeedbackTag.php │ │ ├── FeedbackInteraction.php │ │ ├── FeedbackAttachment.php # added: uuid, disk, collection │ │ └── TicketTransferLog.php # NEW: department-to-department transfer history │ ├── Policies/ │ │ ├── FeedbackPolicy.php # Spatie hasRole() checks │ │ ├── ProductPolicy.php │ │ ├── CustomerPolicy.php │ │ └── FeedbackChannelPolicy.php │ ├── Services/ │ │ ├── FileService.php # Upload, validation, temp URLs, collections │ │ ├── TransferService.php # Cross-dept transfer + notification │ │ └── ClosingService.php # Role-gated closing + evidence check │ ├── Rules/ │ │ └── RequireProofOfResolution.php # Custom validation: proof_of_resolution required for close │ ├── Notifications/ │ │ ├── TicketTransferred.php # DB + Mail for department transfer │ │ └── TicketClosed.php # DB + Mail for ticket closure │ └── Providers/Filament/ │ └── AdminPanelProvider.php ├── resources/views/filament/resources/feedback/ │ ├── similar-cases.blade.php │ └── attachment-links.blade.php # Modal listing attachments with signed download URLs ├── database/ │ ├── migrations/ │ └── seeders/AfterSalesSeeder.php # includes departments, logs, attachments ├── tests/ │ ├── Feature/ │ │ ├── ClosingPermissionTest.php # 4 tests: staff forbid, no-evidence, with-evidence, cross-dept │ │ └── TransferFlowTest.php # 2 tests: successful + missing reason │ └── Pest.php ├── storage/app/private/feedback-attachments/ # Private disk upload directory ├── composer.json ├── PROGRESS.md └── AGENTS.md ``` ## Database Schema ### departments (NEW) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | name | string | | | manager_id | fk -> users nullable | Department head | | created_at, updated_at | timestamp | | ### ticket_transfer_logs (NEW) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | feedback_id | fk -> feedback | cascadeOnDelete | | from_department_id | fk -> departments nullable | | | to_department_id | fk -> departments nullable | | | sender_id | fk -> users | Who initiated transfer | | reason | text | Mandatory | | created_at, updated_at | timestamp | | ### feedback (expanded) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | customer_id | fk -> customers | cascadeOnDelete | | customer_product_id | fk -> customer_product nullable | null = general feedback | | feedback_channel_id | fk -> feedback_channels | | | assigned_to | fk -> users nullable | Current handler | | current_department_id | fk -> departments nullable | **(NEW)** Current department | | is_escalated | boolean default false | **(NEW)** | | title | string | | | content | text | | | status | string | pending, processing, resolved, closed | | created_at, updated_at | timestamp | | ### feedback_attachments (expanded) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | uuid | string nullable unique | **(NEW)** | | feedback_id | fk -> feedback | | | feedback_interaction_id | fk -> feedback_interactions nullable | | | user_id | fk -> users | | | name | string | | | path | string | | | disk | string default 'local' | **(NEW)** | | collection | string default 'general' | **(NEW)** proof_of_resolution, customer_evidence, general | | mime_type | string nullable | | | size | bigint nullable | | | created_at, updated_at | timestamp | | ### Other tables feedback_interactions, feedback_tags, feedback_feedback_tag, products, customers, customer_product, feedback_channels, users (Spatie permission tables: roles, permissions, model_has_roles, etc.) ## Workflow ``` 1. Record Feedback → Assign handler → Assign department 2. Handler contacts customer / does work 3. Handler updates → May forward to another user (assignment interaction) 4. OR: Transfer to another department → TransferDepartment Action (reason required, notification sent) 5. Loop until → Marked as "resolved" (any role) 6. Manager reviews → Close Ticket Action (requires role=admin/manager + proof_of_resolution evidence) ``` ## Key Features ### Transfer Department (NEW - SOP compliant) - Action on EditFeedback page: TransferDepartment - Modal: select target department + reason (required) + optional attachments - Calls TransferService: creates TicketTransferLog, updates current_department_id, resets handler to dept manager - Sends TicketTransferred notification to target department manager ### Close Ticket (NEW - SOP compliant) - Action on EditFeedback page: CloseTicket - Visible only to admin/manager, only when status != 'closed' - Requires confirmation - Staff (403) if tries to close - 422 if no proof_of_resolution attachment exists - Sends TicketClosed notification to handler and department manager ### Status Change in Interactions (Updated) - Staff: only pending, processing, resolved (no "closed" option) - Manager/Admin: all options including "closed" - When "closed" selected: calls ClosingService which validates evidence ### File Management (Updated - SOP compliant) - Files stored on private `local` disk (storage/app/private/feedback-attachments/) - Collection system: `proof_of_resolution`, `customer_evidence`, `general` - FileService validates: jpg, png, pdf, mp4, mov, max 20MB - Temporary signed URLs for private file access - FileService::hasCollection() checks existence of required evidence ### Dashboard Widgets (NEW) - ResolvedTicketsWidget: table of resolved tickets awaiting closure (manager: filtered to own department) - AvgProcessingTimeWidget: 4 stats (resolved count, avg time, pending count, escalated count) - HandlerPerformanceWidget: bar chart of avg processing time per handler ## Navigation Structure ``` Dashboard ├── Customer Care (group) │ └── Feedbacks (chat-bubble-left-right, sort=1) └── Management (group) ├── Products (building-storefront, sort=1) ├── Customers (user-group, sort=2) ├── Channels (inbox-stack, sort=3) └── Tags (tag, sort=default) ``` Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget (admin/manager only) ## RBAC (Role-Based Access Control) - Spatie Roles: `admin`, `manager`, `staff` | Permission | admin | manager | staff | |-----------|-------|---------|-------| | View products/customers/channels/tags | ✓ | ✓ | ✓ | | Create/Update products/customers/channels | ✓ | ✓ | ✗ | | Delete anything | ✓ | ✗ | ✗ | | View feedbacks | ✓ | ✓ | ✓ | | Create/Update feedbacks | ✓ | ✓ | ✓ | | Delete feedbacks | ✓ | ✓ | ✗ | | Add interactions | ✓ | ✓ | ✓ | | Close ticket (status→closed) | ✓ | ✓ | ✗ (403) | | Transfer department | ✓ | ✓ | ✗ | | Dashboard widgets | ✓ | ✓ | ✗ | ## Filament Key Patterns (v5.6) ### Form components use `Filament\Forms\Components` ```php use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Toggle; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\RichEditor; ``` ### Schema/layout components use `Filament\Schemas\Components` ```php use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; ``` ### Services access pattern ```php use Illuminate\Support\Facades\App; $service = App::make(FileService::class); ``` ### Spatie role check ```php $user->hasRole('admin'); $user->hasRole(['admin', 'manager']); $user->assignRole('manager'); ``` ## Known Gotchas 1. **Navigation group icons**: Filament 5 raises exception if both a navigation group AND its child items have icons. 2. **Form component namespaces**: Form input components under `Filament\Forms\Components\*`, layout under `Filament\Schemas\Components\*`. 3. **Dynamic selects**: Use `$get` function injection, NOT `$component->getLivewire()->data[...]`. 4. **Table name**: Model `Feedback` maps to table `feedback` (singular). 5. **FileUpload with dehydrated(false)**: Attachments stored by Filament first; create FeedbackAttachment record directly in after()/afterSave()/afterCreate() — do NOT re-upload via FileService. 6. **Spatie roles in tests**: Create roles explicitly in each test (RefreshDatabase rolls back). 7. **Private disk**: Files use `local` disk (storage/app/private/); access via signed URLs from FileService. 8. **ChartWidget properties**: `$heading` and `$columnSpan` are non-static in Filament's ChartWidget. 9. **Manager closing permission**: Manager must be department head (`Department.manager_id`) to close tickets in that department. 10. **Docker**: Entrypoint auto-generates APP_KEY and runs migrations. SQLite persists via volume mount.