# CODEBASE_SNAPSHOT.md — AfterSales CRM > **Mục đích:** File snapshot toàn bộ cấu trúc codebase hiện tại. Dành cho AI Agent đọc thay vì scan toàn bộ code. > **Cập nhật:** Mỗi khi có thay đổi lớn về cấu trúc (thêm/xóa Model, Migration, Resource, Service, Policy...). --- ## Thông tin chung | Mục | Giá trị | |------|--------| | Framework | Laravel 13.6.0 | | Panels | Filament 5.6.1 — 2 panels: `/admin` (đầy đủ) + `/support` (rút gọn) | | Auth/RBAC | Spatie laravel-permission (3 roles: admin, manager, staff, 29 permissions) | | Database | SQLite (`database/database.sqlite`) | | Testing | Pest PHP | | PHP | 8.3 | | Composer | `/home/phuongtc/composer` | **Test Accounts (seeded):** | Role | Email | Password | |------|-------|----------| | Admin | admin@minicrm.local | password | | Manager | manager@minicrm.local | password | | Staff | staff@minicrm.local | password | --- ## Cây thư mục `app/` ``` app/ ├── Filament/ │ ├── Resources/ │ │ ├── ActivityLogs/ # NEW: Audit trail (read-only) │ │ │ ├── ActivityLogResource.php │ │ │ └── Pages/ListActivityLogs.php │ │ ├── Customers/ │ │ │ ├── CustomerResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateCustomer.php │ │ │ │ ├── EditCustomer.php │ │ │ │ └── ListCustomers.php │ │ │ ├── RelationManagers/ │ │ │ │ └── FeedbacksRelationManager.php # read-only table │ │ │ ├── Schemas/ │ │ │ │ └── CustomerForm.php │ │ │ └── Tables/ │ │ │ └── CustomersTable.php │ │ ├── Feedback/ │ │ │ ├── FeedbackResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateFeedback.php │ │ │ │ ├── EditFeedback.php # TransferDepartment + CloseTicket actions │ │ │ │ └── ListFeedback.php │ │ │ ├── RelationManagers/ │ │ │ │ └── InteractionsRelationManager.php # status_change/note/assignment │ │ │ ├── Schemas/ │ │ │ │ └── FeedbackForm.php │ │ │ └── Tables/ │ │ │ └── FeedbackTable.php │ │ ├── Contracts/ │ │ │ ├── ContractResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateContract.php │ │ │ │ ├── EditContract.php │ │ │ │ └── ListContracts.php │ │ │ ├── Schemas/ │ │ │ │ └── ContractForm.php │ │ │ └── Tables/ │ │ │ └── ContractsTable.php │ │ ├── FeedbackChannels/ │ │ │ ├── FeedbackChannelResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateFeedbackChannel.php │ │ │ │ ├── EditFeedbackChannel.php │ │ │ │ └── ListFeedbackChannels.php │ │ │ ├── Schemas/ │ │ │ │ └── FeedbackChannelForm.php │ │ │ └── Tables/ │ │ │ └── FeedbackChannelsTable.php │ │ ├── FeedbackTags/ │ │ │ ├── FeedbackTagResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateFeedbackTag.php │ │ │ │ ├── EditFeedbackTag.php │ │ │ │ └── ListFeedbackTags.php │ │ │ ├── Schemas/ │ │ │ │ └── FeedbackTagForm.php │ │ │ └── Tables/ │ │ │ └── FeedbackTagsTable.php │ │ ├── Products/ │ │ │ ├── ProductResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateProduct.php │ │ │ │ ├── EditProduct.php │ │ │ │ └── ListProducts.php │ │ │ ├── RelationManagers/ │ │ │ │ ├── ContractsRelationManager.php │ │ │ │ ├── HandoversRelationManager.php │ │ │ │ └── ProductServicesRelationManager.php │ │ │ ├── Schemas/ │ │ │ │ └── ProductForm.php │ │ │ └── Tables/ │ │ │ └── ProductsTable.php │ │ ├── Users/ # Admin-only user management │ │ │ ├── UserResource.php │ │ │ ├── Pages/ │ │ │ │ ├── CreateUser.php │ │ │ │ ├── EditUser.php │ │ │ │ └── ListUsers.php # Tabs: All/Admin/Manager/Staff │ │ │ ├── Schemas/ │ │ │ │ └── UserForm.php # name, email, password, role (dynamic) │ │ │ └── Tables/ │ │ │ └── UsersTable.php # name, email, role badge │ │ └── Roles/ # Admin-only role management │ │ ├── RoleResource.php │ │ ├── Pages/ │ │ │ ├── CreateRole.php │ │ │ ├── EditRole.php │ │ │ └── ListRoles.php │ │ ├── Schemas/ │ │ │ └── RoleForm.php # name + permissions checkbox list │ │ └── Tables/ │ │ └── RolesTable.php # name, permissions count, users count │ └── Widgets/ │ ├── AvgProcessingTimeWidget.php # 4 stats: resolved count, avg time, pending, escalated │ ├── HandlerPerformanceWidget.php # bar chart: avg time per handler │ └── ResolvedTicketsWidget.php # table: resolved tickets awaiting closure ├── Console/ │ └── Commands/ │ ├── ImportCskh.php # app:import-cskh command │ └── ExportBackup.php # app:export-backup command (JSON backup) ├── Enums/ │ ├── ContractCategory.php # V2: ownership/business │ ├── ContractType.php # V2: deposit/purchase/transfer/rental/self_business/bcc │ ├── ContractStatus.php # ACTIVE/EXPIRED/LIQUIDATED │ ├── HandoverStatus.php # NOT_READY/READY/SCHEDULED/HANDED_OVER │ └── ServiceType.php # MANAGEMENT_FEE/GRATITUDE/SELF_BUSINESS ├── Http/ │ ├── Controllers/ │ │ └── Controller.php │ └── Middleware/ │ └── HandleUploadErrors.php ├── Models/ │ ├── ActivityLog.php # NEW: Audit trail (user_id, action, description, metadata) │ ├── Contract.php # V2: +category, +representative_* │ ├── Customer.php # V2: ownedProducts() qua contracts │ ├── Department.php │ ├── Feedback.php │ ├── FeedbackAttachment.php │ ├── FeedbackChannel.php │ ├── FeedbackInteraction.php │ ├── FeedbackTag.php │ ├── Handover.php │ ├── Product.php │ ├── ProductService.php │ ├── TicketTransferLog.php │ └── User.php ├── Notifications/ │ ├── TicketClosed.php │ └── TicketTransferred.php ├── Policies/ │ ├── ActivityLogPolicy.php # NEW: Admin-only │ ├── ContractPolicy.php │ ├── CustomerPolicy.php │ ├── FeedbackChannelPolicy.php │ ├── FeedbackPolicy.php │ ├── ProductPolicy.php │ ├── RolePolicy.php │ └── UserPolicy.php ├── Providers/ │ ├── AppServiceProvider.php │ └── 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 ``` --- ## Dual Panel Architecture ### Admin Panel (`/admin`) — `AdminPanelProvider.php` Đầy đủ tất cả modules: - Resources: Feedback, Customer, Product, Contract, FeedbackChannel, FeedbackTag, User, Role, ActivityLog - Widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget - User/Role management (admin-only) ### Support Panel (`/support`) — `SupportPanelProvider.php` Rút gọn, tập trung feedback: - Resources: Feedback, Customer, Product, FeedbackChannel, FeedbackTag, ActivityLog - Widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget --- ## Models — Chi tiết ### ActivityLog (`activity_logs`) — NEW | Cột | Kiểu | |-----|------| | id | bigint PK | | user_id | FK->users nullable | | action | string (import/export/transfer/close/create/update/delete) | | description | text | | subject_type | string nullable | | subject_id | bigint nullable | | metadata | json nullable | | created_at | timestamp | Relationships: `user()` BelongsTo ### Product (`products`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | description | text nullable | | status | string (active/inactive/sold_out) | Relationships: `contracts()` HasMany, `handovers()` HasMany, `productServices()` HasMany ### ProductService (`product_services`) | Cột | Kiểu | |-----|------| | id | bigint PK | | product_id | FK->products cascadeOnDelete | | service_type | string (management_fee/gratitude/self_business → ServiceType enum) | | status | string (active/pending/completed/cancelled) | | actual_collection_date | date nullable | | remark | text nullable | Relationships: `product()` BelongsTo ### Handover (`handovers`) | Cột | Kiểu | |-----|------| | id | bigint PK | | product_id | FK->products cascadeOnDelete | | customer_id | FK->customers cascadeOnDelete | | handover_date | date nullable | | status | string (chua_du_dieu_kien/du_dieu_kien/da_len_lich/da_ban_giao → HandoverStatus enum) | | remark | text nullable | Relationships: `product()` BelongsTo, `customer()` BelongsTo ### Contract (`contracts`) — V2 | Cột | Kiểu | |-----|------| | id | bigint PK | | product_id | FK->products cascadeOnDelete | | customer_id | FK->customers cascadeOnDelete | | category | string (ownership/business → ContractCategory enum) | | type | string (deposit/purchase/transfer/rental/self_business/bcc → ContractType enum) | | start_date | date nullable | | end_date | date nullable | | status | string (active/expired/liquidated → ContractStatus enum) | | printed_at | date nullable | | signed_status | string nullable | | representative_name | string nullable | | representative_phone | string nullable | | representative_email | string nullable | Relationships: `product()` BelongsTo, `customer()` BelongsTo, `feedbacks()` HasMany > **Constraint:** Mỗi product tối đa 1 active contract mỗi category. Validation ở Model `booted::saving`. ### Customer (`customers`) — V2 | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | email | string nullable unique | | phone | string nullable | | address | text nullable | | status | string (active) | Relationships: `ownedProducts()` BelongsToMany qua contracts (ownership + active), `feedbacks()` HasMany ### Department (`departments`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | manager_id | FK->users nullable nullOnDelete | Relationships: `manager()` BelongsTo User, `feedbacks()` HasMany, `transferLogsFrom()` HasMany, `transferLogsTo()` HasMany ### User (`users`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | email | string unique | | password | string | | role | string (admin/manager/staff) | Uses Spatie `HasRoles`. Relationships: `assignedFeedbacks()` HasMany Feedback, `activityLogs()` HasMany. ### Feedback (`feedback`) — V2 | Cột | Kiểu | |-----|------| | id | bigint PK | | customer_id | FK->customers cascadeOnDelete | | contract_id | FK->contracts nullable nullOnDelete | | feedback_channel_id | FK->feedback_channels | | assigned_to | FK->users nullable nullOnDelete | | current_department_id | FK->departments nullable nullOnDelete | | is_escalated | boolean default false | | title | string | | content | text | | status | string (pending/processing/resolved/closed) | Fillable: `['customer_id', 'contract_id', 'feedback_channel_id', 'assigned_to', 'title', 'content', 'status', 'current_department_id', 'is_escalated']` Relationships: `customer()`, `contract()`, `feedbackChannel()`, `assignedTo()`, `currentDepartment()`, `transferLogs()`, `interactions()` ordered latest, `attachments()`, `tags()` BelongsToMany ### FeedbackInteraction (`feedback_interactions`) | Cột | Kiểu | |-----|------| | id | bigint PK | | feedback_id | FK->feedback cascadeOnDelete | | user_id | FK->users | | type | string (note/status_change/assignment) | | content | text nullable | | changes | json nullable | Relationships: `feedback()` BelongsTo, `user()` BelongsTo, `attachments()` HasMany ### FeedbackAttachment (`feedback_attachments`) | Cột | Kiểu | |-----|------| | id | bigint PK | | uuid | uuid nullable unique | | feedback_id | FK->feedback cascadeOnDelete | | feedback_interaction_id | FK->feedback_interactions nullable nullOnDelete | | 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 | Relationships: `feedback()`, `interaction()`, `user()`. Scopes: `byCollection()`, `byDisk()`. ### FeedbackChannel (`feedback_channels`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | slug | string unique | | icon | string nullable | | color | string nullable | | is_active | boolean default true | Relationships: `feedbacks()` HasMany ### FeedbackTag (`feedback_tags`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | slug | string unique | | color | string nullable | Relationships: `feedbacks()` BelongsToMany (pivot: feedback_feedback_tag) ### TicketTransferLog (`ticket_transfer_logs`) | Cột | Kiểu | |-----|------| | id | bigint PK | | feedback_id | FK->feedback cascadeOnDelete | | from_department_id | FK->departments nullable nullOnDelete | | to_department_id | FK->departments nullable nullOnDelete | | sender_id | FK->users | | reason | text | Relationships: `feedback()`, `fromDepartment()`, `toDepartment()`, `sender()` --- ## Services — Tóm tắt ### ActivityLogger (`app/Services/ActivityLogger.php`) — NEW - `ActivityLogger::log(action, description, subject?, metadata?, userId?)`: Static method. Tạo ActivityLog record cho audit trail. Dùng trong Import/Export/Transfer/Close/Create/Update/Delete. ### TransferService (`app/Services/TransferService.php`) - `transfer(Feedback, Department, reason, User, ?int $newHandlerId)`: Tạo TicketTransferLog, cập nhật current_department_id + assigned_to, gửi TicketTransferred notification cho manager đích. - Validate reason không được rỗng (ValidationException). ### FileService (`app/Services/FileService.php`) - `upload()`: Validate (jpg/png/pdf/mp4/mov, ≤20MB), store vào public disk, tạo FeedbackAttachment record. - `getTemporaryUrl()` / `getPreviewUrl()`: Signed URL cho private file. - `getByCollection()` / `hasCollection()`: Query collection-based attachments. ### ClosingService (`app/Services/ClosingService.php`) - `close(Feedback, User)`: 4 guard: status ≠ closed, staff → 403, manager chỉ đóng ticket phòng mình, phải có proof_of_resolution attachment (FileService::hasCollection). Gửi TicketClosed notification. - `resolve(Feedback, User)`: Đánh dấu resolved (không role gate). --- ## Policies — Tóm tắt > **Note:** Policies use Spatie `hasPermissionTo()` instead of `hasRole()` for granular permission control. | Policy | viewAny/view | create | update | delete | restore/forceDelete | |--------|-------------|--------|--------|--------|---------------------| | ActivityLogPolicy | admin only | — | — | — | — | | ContractPolicy | view-contract | create-contract | update-contract | delete-contract | admin only | | FeedbackPolicy | view-feedback | create-feedback | update-feedback | delete-feedback | admin only | | ProductPolicy | view-product | create-product | update-product | delete-product | admin only | | CustomerPolicy | view-customer | create-customer | update-customer | delete-customer | admin only | | FeedbackChannelPolicy | view-channel | create-channel | update-channel | delete-channel | admin only | | UserPolicy | admin only | admin only | admin only | admin only (not self) | admin only (not self) | | RolePolicy | admin only | admin only | admin only | admin only (not built-in) | admin only (not built-in) | --- ## Filament Navigation Structure ### Admin Panel (`/admin`) ``` Dashboard (AccountWidget + 3 custom widgets) ├── Customer Care (group) │ └── Feedbacks (heroicon-o-chat-bubble-left-right, sort=1) └── Management (group) ├── Products (heroicon-o-building-storefront, sort=1) ├── Customers (heroicon-o-user-group, sort=2) ├── Channels (heroicon-o-inbox-stack, sort=3) ├── Contracts (heroicon-o-document-text, sort=4) ├── Tags (heroicon-o-tag, default sort) ├── Activity Logs (heroicon-o-clock, sort=7) — admin only ├── Users (heroicon-o-users, sort=5) — admin only └── Roles (heroicon-o-shield-check, sort=6) — admin only ``` ### Support Panel (`/support`) ``` Dashboard (3 custom widgets) ├── Customer Care (group) │ └── Feedbacks └── Management (group) ├── Products ├── Customers ├── Channels ├── Tags └── Activity Logs ``` --- ## Key Filament Actions trên EditFeedback 1. **TransferDepartment** — Modal: Chọn phòng ban đích + reason (required) + attachments (optional). Visibility: admin/manager. 2. **CloseTicket** — Confirm modal. Visibility: admin/manager, status ≠ closed. --- ## Database Migration files (25 files) | # | File | Tables | |---|------|--------| | 1 | `0001_01_01_000000` | users, password_reset_tokens, sessions | | 2 | `0001_01_01_000001` | cache, cache_locks | | 3 | `0001_01_01_000002` | jobs, job_batches, failed_jobs | | 4 | `2026_04_26_052423_create_products_table` | products | | 5 | `2026_04_26_052423_create_customers_table` | customers | | 6 | `2026_04_26_052423_create_customer_product_table` | customer_product (pivot) | | 7 | `2026_04_26_052424_create_feedback_channels_table` | feedback_channels | | 8 | `2026_04_26_052424_create_feedbacks_table` | feedback (base) | | 9 | `2026_04_26_052528_add_role_to_users_table` | (modify users) add role | | 10 | `2026_04_26_063709_create_feedback_interactions_table` | feedback_interactions | | 11 | `2026_04_26_063710_create_feedback_attachments_table` | feedback_attachments (base) | | 12 | `2026_04_26_070225_create_feedback_tags_table` | feedback_tags, feedback_feedback_tag | | 13 | `2026_04_27_030125_create_permission_tables` | Spatie: permissions, roles, model_has_roles, etc. | | 14 | `2026_04_27_080000_create_departments_table` | departments | | 15 | `2026_04_27_081000_add_department_to_feedback_table` | (modify feedback) add current_department_id, is_escalated | | 16 | `2026_04_27_082000_create_ticket_transfer_logs_table` | ticket_transfer_logs | | 17 | `2026_04_27_083000_add_collection_to_feedback_attachments_table` | (modify feedback_attachments) add uuid, disk, collection | | 18 | `2026_04_27_140000_fix_attachment_disk_default` | (modify feedback_attachments) fix disk default private→local | | 19 | `2026_04_27_150000_create_notifications_table` | notifications | | 20 | `2026_05_01_000000_create_contracts_table` | contracts | | 21 | `2026_05_01_000001_add_contract_id_to_feedback_table` | (modify feedback) add contract_id FK | | 22 | `2026_05_01_000002_create_handovers_table` | handovers | | 23 | `2026_05_01_000003_create_product_services_table` | product_services | | 24 | `2026_05_01_000004_add_color_to_feedback_channels_table` | (modify feedback_channels) add color | | 25 | `2026_05_09_104519_create_activity_logs_table` | activity_logs | | 26 | `2026_05_12_075014_v2_restructure_contracts` | **(V2)** contracts +category, +representative; drop customer_product; drop feedback.customer_product_id | --- ## Seeder (AfterSalesSeeder + PermissionSeeder) — Data mẫu - 3 users (admin, manager, staff) + Spatie roles - **29 granular permissions** (PermissionSeeder) - Feedback: view/create/update/delete, add-interaction, close-ticket, transfer-department - Products/Customers/Contracts/Channels/Tags: view/create/update/delete each - Dashboard: view-dashboard-widgets, export-data - Role → Permission mapping: admin (all), manager (no delete/export), staff (view + limited write) - 4 departments (CSKH: manager quản lý, Kỹ thuật: admin quản lý, Kế toán, Pháp lý) - 5 feedback channels (Email, Zalo, Phone, Document, In Person) - 5 products (Sunrise A1, A2, Green Valley Villa, Ocean Tower, Park Hill) - 4 customers - 5 customer-product assignments - **5 contracts** (2 Sale, 2 Lease, 1 BCC) - **5 handovers** (2 Đã BG, 1 Đủ ĐK, 1 Đã lên lịch, 1 Chưa đủ ĐK) - **5 product_services** (2 Management Fee, 2 Gratitude, 1 Self Business) - 5 feedbacks (pending: 2, processing: 1, resolved: 1, closed: 1) - 5 feedback tags - 5 feedback interactions - 1 ticket_transfer_log - 3 feedback_attachments (2 proof_of_resolution, 1 customer_evidence) --- ## Data Import (`app:import-cskh`) Command: `php artisan app:import-cskh {file_path}` - Sử dụng `spatie/simple-excel` với Lazy Collection (chống OOM) - ProgressBar + chunk 100 rows/transaction - Pipeline mỗi dòng: 1. `firstOrCreate` Product (Mã căn hộ) + Customer (Họ tên KH) 2. Insert Contract, Handover, ProductService nếu có dữ liệu 3. Tách `Lịch Sử khách hàng` → Feedback + FeedbackInteraction 4. Auto-gắn active contract vào Feedback - Hỗ trợ cột: Mã căn hộ, Họ tên KH, BÀN GIÀO CĂN HỘ, TÌNH TRẠNG, HỢP ĐỒNG THUÊ, HĐ HỢP TÁC KINH DOANH, TỰ DOANH, Phí QL, Lịch Sử khách hàng --- **Test files:** `tests/Feature/ClosingPermissionTest.php`, `tests/Feature/TransferFlowTest.php` | Test | Coverage | |------|----------| | ClosingPermissionTest (4 tests) | staff close → 403, close without evidence → 422, close with evidence → OK, cross-department manager → 403 | | TransferFlowTest (2 tests) | successful transfer, missing reason | Tổng: 8 tests, 21 assertions. --- ## Các Pattern cần biết khi code 1. **Form components:** `Filament\Forms\Components\*` (Select, TextInput, Textarea, Toggle, FileUpload, RichEditor) 2. **Schema/layout:** `Filament\Schemas\Components\*` (Section, Schema) 3. **Service access:** `App::make(FileService::class)` hoặc inject constructor. `ActivityLogger::log()` là static. 4. **Role check:** `$user->hasRole('admin')` / `$user->hasRole(['admin', 'manager'])` 5. **Dynamic form field:** dùng `$get` function injection, KHÔNG dùng `$component->getLivewire()->data` 6. **FileUpload với dehydrated(false):** File được Filament lưu trước, chỉ cần tạo FeedbackAttachment record trong after()/afterSave() — KHÔNG re-upload qua FileService 7. **Private disk:** files lưu ở `storage/app/private/feedback-attachments/`, truy cập qua signed URL từ FileService. Luôn dùng `disk('public')` cho FileUpload. 8. **Navigation group icons:** Filament 5 báo lỗi nếu cả navigation group VÀ child items đều có icons 9. **Dual Panel:** Cả 2 panel share models, services, policies, database, translations --- ## File hướng dẫn liên quan | File | Nội dung | |------|----------| | `AGENTS.md` | Hướng dẫn cho AI Agent: cách chạy, cấu trúc, workflow, RBAC, gotchas, patterns | | `TASKS_ROADMAP.md` | Roadmap + lịch sử tiến độ hoàn thành | | `docs/I18N_GUIDE.md` | Hướng dẫn đa ngôn ngữ chi tiết | | `docs/ENUM_GUIDE.md` | Hướng dẫn thêm Enum mới | | `UI_DESIGN_BRIEF.md` | Thiết kế UI chi tiết (horizontal top bar) | | `webappUI/aftersales_crm_core/DESIGN.md` | Design tokens (colors, typography, spacing) | | `luutru/` | Archive các file đã lỗi thời (tham khảo nếu cần) |