# 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 > - `docs/I18N_GUIDE.md` — Hướng dẫn đa ngôn ngữ > - `docs/ENUM_GUIDE.md` — Hướng dẫn thêm Enum mới > - `luutru/` — Archive các file đã lỗi thời (tham khảo nếu cần) > > **File đã lỗi thời trong luutru/:** PROGRESS.md, SOP_des.md, AI_01/02/03, CHANGELOG.md, README.md gốc, SYSTEM_ASSESSMENT_PROPOSALS.md ## Overview AfterSales CRM is a real-estate after-sales customer care system built with **Laravel 13.6** + **Filament 5.6**. Hệ thống có **2 Filament Panel**: - `/admin` — Admin Panel: đầy đủ tất cả modules (Contract, Handover, Service, User/Role...) - `/support` — Support Panel: rút gọn, tập trung Feedback (Feedback, Customer, Product, Channel, Tag, ActivityLog) ## How to Run ```bash cd /home/phuongtc/vibecode/minicrm php artisan migrate:fresh --seed php artisan serve # Admin panel: http://localhost:8000/admin # Support panel: http://localhost:8000/support ``` ### 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 # Export database backup (JSON format, one file per table) php artisan app:export-backup php artisan app:export-backup --path=/custom/path --pretty php artisan app:export-backup --tables=feedback,customers,products # Import data from Excel php artisan app:import-cskh ai_instructions/CSKH.xlsx --dry-run php artisan app:import-cskh ai_instructions/CSKH.xlsx ``` ## Dual Panel Architecture Hệ thống có 2 Filament Panel độc lập: ### Admin Panel (`/admin`) — `AdminPanelProvider.php` Đầy đủ tất cả modules, bao gồm: - Feedback, Customer, Product, Contract, FeedbackChannel, FeedbackTag - User, Role (admin-only) - 3 Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget ### Support Panel (`/support`) — `SupportPanelProvider.php` Rút gọn, chỉ tập trung vào quy trình xử lý feedback: - Feedback, Customer, Product, FeedbackChannel, FeedbackTag, ActivityLog - 3 Dashboard widgets giống Admin panel - Không có: Contracts, Users, Roles, Handovers, ProductServices Cả 2 panel dùng chung: Models, Services, Policies, Database, Translations. ## Directory Structure ``` minicrm/ ├── app/ │ ├── Filament/ │ │ ├── Resources/ │ │ │ ├── ActivityLogs/ # NEW: Audit trail (read-only) │ │ │ │ ├── ActivityLogResource.php │ │ │ │ └── Pages/ListActivityLogs.php │ │ │ ├── Feedback/ │ │ │ │ ├── FeedbackResource.php │ │ │ │ ├── Schemas/FeedbackForm.php │ │ │ │ ├── Tables/FeedbackTable.php │ │ │ │ ├── Pages/{Create,Edit,List}Feedback.php │ │ │ │ └── RelationManagers/InteractionsRelationManager.php │ │ │ ├── Products/ │ │ │ │ └── RelationManagers/{Contracts,Handovers,ProductServices}RelationManager.php │ │ │ ├── Customers/ │ │ │ │ └── RelationManagers/FeedbacksRelationManager.php │ │ │ ├── Contracts/ │ │ │ ├── FeedbackChannels/ │ │ │ ├── FeedbackTags/ │ │ │ ├── Users/ │ │ │ └── Roles/ │ │ └── Widgets/ │ │ ├── ResolvedTicketsWidget.php # Dashboard: resolved tickets table │ │ ├── AvgProcessingTimeWidget.php # Dashboard: stats overview │ │ └── HandlerPerformanceWidget.php # Dashboard: bar chart per handler │ ├── Console/Commands/ │ │ ├── ImportCskh.php # app:import-cskh │ │ └── ExportBackup.php # app:export-backup │ ├── Enums/ │ │ ├── ContractCategory.php # V2: ownership/business │ │ ├── ContractType.php │ │ ├── ContractStatus.php │ │ ├── HandoverStatus.php │ │ └── ServiceType.php │ ├── Http/ │ │ ├── Controllers/Controller.php │ │ └── Middleware/HandleUploadErrors.php │ ├── Models/ │ │ ├── ActivityLog.php # NEW: Audit trail model │ │ ├── Contract.php # V2: +category, +representative_* │ │ ├── Customer.php # V2: ownedProducts() qua contracts │ │ ├── Department.php │ │ ├── Feedback.php # V2: bỏ customer_product_id │ │ ├── FeedbackAttachment.php │ │ ├── FeedbackChannel.php │ │ ├── FeedbackInteraction.php │ │ ├── FeedbackTag.php │ │ ├── Handover.php │ │ ├── Product.php │ │ ├── ProductService.php │ │ ├── TicketTransferLog.php │ │ └── User.php │ ├── Policies/ │ │ ├── ActivityLogPolicy.php # NEW │ │ ├── ContractPolicy.php │ │ ├── CustomerPolicy.php │ │ ├── FeedbackChannelPolicy.php │ │ ├── FeedbackPolicy.php │ │ ├── ProductPolicy.php │ │ ├── RolePolicy.php │ │ └── UserPolicy.php │ ├── Notifications/ │ │ ├── TicketTransferred.php │ │ └── TicketClosed.php │ ├── Providers/Filament/ │ │ ├── AdminPanelProvider.php # Panel đầy đủ (/admin) │ │ └── SupportPanelProvider.php # Panel rút gọn (/support) │ ├── Rules/ │ │ └── RequireProofOfResolution.php │ └── Services/ │ ├── ActivityLogger.php # NEW: static log() cho audit trail │ ├── ClosingService.php │ ├── FileService.php │ └── TransferService.php ├── lang/ # i18n translation files │ ├── en/{app.php, feedback.php, enums.php} │ └── vi/{app.php, feedback.php, enums.php} ├── resources/views/filament/resources/feedback/ │ ├── similar-cases.blade.php │ └── attachment-links.blade.php ├── webappUI/aftersales_crm_core/ # Design tokens & system │ └── DESIGN.md ├── database/ │ ├── migrations/ # 25 migrations │ └── seeders/{AfterSalesSeeder,PermissionSeeder}.php ├── tests/Feature/ │ ├── ClosingPermissionTest.php │ └── TransferFlowTest.php ├── luutru/ # Archive các file đã lỗi thời ├── UI_DESIGN_BRIEF.md # Thiết kế UI chi tiết └── TASKS_ROADMAP.md ``` ## Database Schema ### activity_logs (NEW) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | user_id | fk -> users nullable | Who performed action | | action | string | import, export, transfer, close, create, update, delete | | description | text | Human-readable description | | subject_type | string nullable | Related model class | | subject_id | bigint nullable | Related model ID | | metadata | json nullable | Extra data | | created_at | timestamp | | ### contracts (V2: category + representative) | Column | Type | Notes | |--------|------|-------| | id | bigint PK | | | product_id | fk -> products | cascadeOnDelete | | customer_id | fk -> customers | cascadeOnDelete | | category | string | **(V2)** ownership \| business | | type | string | **(V2)** deposit, purchase, transfer, rental, self_business, bcc | | start_date, end_date | date nullable | | | status | string | active, expired, liquidated | | printed_at | date nullable | | | signed_status | string nullable | | | representative_name | string nullable | **(V2)** | | representative_phone | string nullable | **(V2)** | | representative_email | string nullable | **(V2)** | | created_at, updated_at | timestamp | | > **V2 Constraint:** Mỗi product chỉ có tối đa 1 contract active mỗi category. Validation ở cả Model level (`booted::saving`) và UI. ### 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 | 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 | | contract_id | fk -> contracts nullable | **(V2)** Snapshot contract (thay thế customer_product_id) | | feedback_channel_id | fk -> feedback_channels | | | assigned_to | fk -> users nullable | Current handler | | current_department_id | fk -> departments nullable | Current department | | is_escalated | boolean default false | | | 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 | | | feedback_id | fk -> feedback | | | feedback_interaction_id | fk -> feedback_interactions nullable | | | user_id | fk -> users | | | name | string | | | path | string | | | disk | string default 'local' | | | collection | string default 'general' | 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, feedback_channels, users, contracts, handovers, product_services, roles, permissions, model_has_roles (Spatie), notifications, sessions, cache ## 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 ### Dual Panel System (NEW) - **Admin Panel** (`/admin`): Đầy đủ modules, User/Role management - **Support Panel** (`/support`): Rút gọn, tập trung Feedback. Dùng cho đội support không cần quản lý hệ thống ### ActivityLog — Audit Trail (NEW) - Tự động ghi log các hành động: import, export, transfer, close, create, update, delete - `ActivityLogger::log(action, description, subject, metadata, userId)` - Xem tại `/admin/activity-logs` hoặc `/support/activity-logs` - Admin-only access (ActivityLogPolicy) ### Transfer Department (SOP compliant) - Action trên EditFeedback: 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 (SOP compliant) - Action trên EditFeedback: 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 (SOP compliant) - Files stored on `public` disk (`storage/app/public/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 - 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 ## Internationalization (i18n) ### Configuration - Default locale: `vi` (Vietnamese), fallback: `en` (English) - Set in `.env`: `APP_LOCALE=vi`, `APP_FALLBACK_LOCALE=en` - Filament uses `app()->getLocale()` — no extra panel config needed ### Translation Files ``` lang/ ├── en/ │ ├── app.php # General UI: navigation, labels, status, widgets, blade strings │ ├── feedback.php # Feedback domain: services, notifications, interactions │ └── enums.php # Enum labels (HandoverStatus, ServiceType, ContractStatus, ContractType) └── vi/ ├── app.php ├── feedback.php └── enums.php ``` ### Translation Patterns ```php // Resource labels — MUST override getPluralLabel() (not auto-translated) public static function getPluralLabel(): ?string { return __('app.resource_feedbacks'); } // Navigation group — MUST override getNavigationGroup() (not auto-translated) public static function getNavigationGroup(): ?string { return __('app.nav_customer_care'); } // Form fields — MUST add explicit ->label() TextInput::make('name')->label(__('app.name')) // Status options 'options' => [ 'pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), ] // Enum labels public function label(): string { return match ($this) { self::ACTIVE => __('enums.contract_status.active'), }; } // Widget heading/description — override getHeading()/getDescription() public function getHeading(): string | Htmlable | null { return __('app.widget_avg_time_by_handler'); } ``` ### Key Gotchas - `$pluralLabel` property does NOT auto-translate — must override `getPluralLabel()` - `getNavigationGroup()` does NOT auto-translate — must call `__()` explicitly - `$heading`/`$description` in ChartWidget are raw properties — must override `getHeading()`/`getDescription()` - Form fields without `->label()` use Filament's `filament-panels::` namespace (may not resolve correctly) - Always add `->label(__('key'))` to TextInput, Select, Textarea, etc. ## Navigation Structure ### Admin Panel (`/admin`) ``` Dashboard (AccountWidget + 3 custom widgets) ├── Customer Care / Chăm sóc khách hàng (group) │ └── Feedbacks / Phản ánh (chat-bubble-left-right, sort=1) └── Management / Quản lý (group) ├── Products / Sản phẩm (building-storefront, sort=1) ├── Customers / Khách hàng (user-group, sort=2) ├── Channels / Kênh phản ánh (inbox-stack, sort=3) ├── Contracts / Hợp đồng (document-text, sort=4) ├── Tags / Thẻ (tag, sort=default) ├── Activity Logs (clock, sort=7) — admin only ├── Users / Người dùng (users, sort=5) — admin only └── Roles / Vai trò (shield-check, sort=6) — admin only ``` ### Support Panel (`/support`) ``` Dashboard (3 custom widgets) ├── Customer Care / Chăm sóc khách hàng (group) │ └── Feedbacks └── Management / Quản lý (group) ├── Products ├── Customers ├── Channels ├── Tags └── Activity Logs ``` Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget (admin/manager only) ## RBAC (Role-Based Access Control) - Spatie Roles: `admin`, `manager`, `staff` ### Granular Permissions (Spatie Permission) **Feedback:** `view-feedback`, `create-feedback`, `update-feedback`, `delete-feedback`, `add-interaction`, `close-ticket`, `transfer-department` **Products:** `view-product`, `create-product`, `update-product`, `delete-product` **Customers:** `view-customer`, `create-customer`, `update-customer`, `delete-customer` **Contracts:** `view-contract`, `create-contract`, `update-contract`, `delete-contract` **Channels:** `view-channel`, `create-channel`, `update-channel`, `delete-channel` **Tags:** `view-tag`, `create-tag`, `update-tag`, `delete-tag` **Dashboard:** `view-dashboard-widgets`, `export-data` ### Permission Matrix | Permission | admin | manager | staff | |-----------|-------|---------|-------| | view-feedback | ✓ | ✓ | ✓ | | create-feedback | ✓ | ✓ | ✓ | | update-feedback | ✓ | ✓ | ✓ | | delete-feedback | ✓ | ✓ | ✗ | | add-interaction | ✓ | ✓ | ✓ | | close-ticket | ✓ | ✓ | ✗ | | transfer-department | ✓ | ✓ | ✗ | | view-product | ✓ | ✓ | ✓ | | create/update-product | ✓ | ✓ | ✗ | | delete-product | ✓ | ✗ | ✗ | | view-customer | ✓ | ✓ | ✓ | | create/update-customer | ✓ | ✓ | ✗ | | delete-customer | ✓ | ✗ | ✗ | | view-contract | ✓ | ✓ | ✓ | | create/update-contract | ✓ | ✓ | ✗ | | delete-contract | ✓ | ✗ | ✗ | | view-channel/tag | ✓ | ✓ | ✓ | | create/update-channel/tag | ✓ | ✓ | ✗ | | delete-channel/tag | ✓ | ✗ | ✗ | | view-dashboard-widgets | ✓ | ✓ | ✗ | | export-data | ✓ | ✗ | ✗ | ### Permission check patterns ```php // In Policies $user->hasPermissionTo('close-ticket'); $user->hasPermissionTo('view-feedback'); // In Filament auth()->user()->hasPermissionTo('transfer-department'); auth()->user()->hasPermissionTo('view-dashboard-widgets'); // Role check (still used for admin-only guards) $user->hasRole('admin'); ``` ## 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); // ActivityLogger (static) \App\Services\ActivityLogger::log('transfer', '...', $feedback, ['dept' => 'CSKH']); ``` ### 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. 11. **i18n - pluralLabel**: `$pluralLabel` property does NOT auto-translate — must override `getPluralLabel()` and call `__()`. 12. **i18n - navigationGroup**: `getNavigationGroup()` does NOT auto-translate — must return `__('key')` explicitly. 13. **i18n - ChartWidget heading**: `$heading`/`$description` are raw properties — must override `getHeading()`/`getDescription()`. 14. **i18n - form labels**: Form fields without `->label()` use Filament's `filament-panels::` namespace — always add explicit `->label(__('key'))`. 15. **i18n - Filament vendor**: Filament ships 57 Vietnamese translation files — auto-activated when `APP_LOCALE=vi`. 16. **FileUpload disk**: Always use `disk('public')` for all FileUpload components. NEVER use `disk('local')` — causes file not found errors. 17. **FileUpload dehydrated**: Do NOT use `dehydrated(false)` — prevents file paths from being available in afterSave()/afterCreate(). Use property to store paths in mutateFormDataBeforeSave(). 18. **File metadata safety**: Always check `$disk->exists($path)` before calling `$disk->mimeType()` or `$disk->size()` — prevents UnableToRetrieveMetadata error. 19. **FileUpload in Actions**: When handling file uploads in Action callbacks (e.g., transfer attachments), file paths are strings (not UploadedFile). Create records directly instead of using FileService::upload(). 20. **Tailwind CSS in Filament**: Custom blade views render inside Filament layout which does NOT include full Tailwind CSS build. Must add needed utilities to `public/css/filament-custom.css` with `!important`. 21. **SVG icon sizing**: Use inline `width`/`height` attributes on SVG icons (e.g., ``) instead of Tailwind classes (`w-3.5 h-3.5`) — prevents Filament CSS from overriding sizes. 22. **Dual Panel**: Support panel at `/support` is separate from Admin panel at `/admin`. Both share the same models/services/policies/database. ## File Upload Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ FileUpload Component (disk='public') │ │ → Filament uploads via AJAX to storage/app/public/ │ │ → File path stored in Livewire state │ │ │ │ Form Submit │ │ → mutateFormDataBeforeSave() stores paths in property │ │ → Removes 'attachments' from $data before saving model │ │ │ │ afterSave()/afterCreate() │ │ → Uses stored paths to create FeedbackAttachment records │ │ → Checks $disk->exists($path) before getting metadata │ │ → Uses asset('storage/' . $path) for URLs │ └─────────────────────────────────────────────────────────────────┘ ``` ### FileUpload Pattern (correct): ```php // In Form FileUpload::make('attachments') ->disk('public') // ALWAYS public ->directory('feedback-attachments') // Do NOT use ->dehydrated(false) // In Page class protected array $pendingAttachments = []; protected function mutateFormDataBeforeSave(array $data): array { $this->pendingAttachments = $data['attachments'] ?? []; unset($data['attachments']); // Remove before saving model return $data; } protected function afterSave(): void { $disk = Storage::disk('public'); foreach ($this->pendingAttachments as $path) { if (! $disk->exists($path)) continue; // Safety check $this->getRecord()->attachments()->create([ 'path' => $path, 'disk' => 'public', 'mime_type' => $disk->mimeType($path), 'size' => $disk->size($path), // ... ]); } } ``` ### Tailwind CSS in Filament (correct): ```php // Filament uses its own CSS build, NOT the project's Tailwind build // Custom blade views need utilities added to public/css/filament-custom.css // In filament-custom.css: .flex { display: flex !important; } .items-center { align-items: center !important; } .gap-2 { gap: 0.5rem !important; } // ... add all needed utilities with !important // For SVG icons, use inline attributes: ... // NOT: ... ```