# 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 | | Admin Panel | Filament 5.6.1 | | Auth/RBAC | Spatie laravel-permission (3 roles: admin, manager, staff) | | 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/ │ │ ├── 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/ # NEW │ │ │ ├── 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 # NEW │ │ │ ├── HandoversRelationManager.php # NEW │ │ │ └── ProductServicesRelationManager.php # NEW │ │ ├── Schemas/ │ │ │ └── ProductForm.php │ │ └── Tables/ │ │ └── ProductsTable.php │ └── 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/ # NEW │ └── Commands/ │ └── ImportCskh.php # app:import-cskh command ├── Enums/ # NEW │ ├── ContractType.php # SALE/LEASE/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 ├── Models/ │ ├── Contract.php # NEW │ ├── Customer.php │ ├── CustomerProduct.php # pivot model (customer_product table) │ ├── Department.php │ ├── Feedback.php │ ├── FeedbackAttachment.php │ ├── FeedbackChannel.php │ ├── FeedbackInteraction.php │ ├── FeedbackTag.php │ ├── Handover.php # NEW │ ├── Product.php │ ├── ProductService.php # NEW │ ├── TicketTransferLog.php │ └── User.php ├── Notifications/ │ ├── TicketClosed.php │ └── TicketTransferred.php ├── Policies/ │ ├── ContractPolicy.php # NEW │ ├── CustomerPolicy.php │ ├── FeedbackChannelPolicy.php │ ├── FeedbackPolicy.php │ └── ProductPolicy.php ├── Providers/ │ ├── AppServiceProvider.php │ └── Filament/ │ └── AdminPanelProvider.php ├── Rules/ │ └── RequireProofOfResolution.php └── Services/ ├── ClosingService.php ├── FileService.php └── TransferService.php ``` --- ## Models — Chi tiết ### Product (`products`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | description | text nullable | | status | string (active/inactive/sold_out) | Relationships: `customers()` BelongsToMany (pivot: customer_product, withPivot: purchase_date), `contracts()` HasMany, `handovers()` HasMany, `productServices()` HasMany ### ProductService (`product_services`) — NEW | 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`) — NEW | 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`) — NEW | Cột | Kiểu | |-----|------| | id | bigint PK | | product_id | FK->products cascadeOnDelete | | customer_id | FK->customers cascadeOnDelete | | type | string (sale/lease/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 | Relationships: `product()` BelongsTo, `customer()` BelongsTo, `feedbacks()` HasMany ### Customer (`customers`) | Cột | Kiểu | |-----|------| | id | bigint PK | | name | string | | email | string nullable unique | | phone | string nullable | | address | text nullable | | status | string (active) | Relationships: `products()` BelongsToMany, `feedbacks()` HasMany ### CustomerProduct (`customer_product` — pivot model) | Cột | Kiểu | |-----|------| | id | bigint PK | | customer_id | FK->customers cascadeOnDelete | | product_id | FK->products cascadeOnDelete | | purchase_date | date nullable | UNIQUE(customer_id, product_id) Relationships: `customer()` BelongsTo, `product()` BelongsTo, `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. ### Feedback (`feedback`) | Cột | Kiểu | |-----|------| | id | bigint PK | | customer_id | FK->customers cascadeOnDelete | | customer_product_id | FK->customer_product nullable nullOnDelete | | 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', 'customer_product_id', 'contract_id', 'feedback_channel_id', 'assigned_to', 'title', 'content', 'status', 'current_department_id', 'is_escalated']` Relationships (10): `customer()`, `customerProduct()`, `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 | | 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 ### 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). ### 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). ### FileService (`app/Services/FileService.php`) - `upload()`: Validate (jpg/png/pdf/mp4/mov, ≤20MB), store vào private local disk (`feedback-attachments/`), tạo FeedbackAttachment record. - `getTemporaryUrl()` / `getPreviewUrl()`: Signed URL cho private file. - `getByCollection()` / `hasCollection()`: Query collection-based attachments. --- ## Policies — Tóm tắt | Policy | viewAny/view | create | update | delete | restore/forceDelete | |--------|-------------|--------|--------|--------|---------------------| | ContractPolicy | all roles | admin+manager | admin+manager | admin | admin only | | FeedbackPolicy | all roles | all roles | all roles | admin+manager | admin only | | ProductPolicy | all roles | admin+manager | admin+manager | admin | admin only | | CustomerPolicy | all roles | admin+manager | admin+manager | admin | admin only | | FeedbackChannelPolicy | all roles | admin+manager | admin+manager | admin | admin only | --- ## Filament Navigation Structure ``` 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) # NEW └── Tags (heroicon-o-tag, default sort) ``` --- ## 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 (23 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` | **(NEW)** contracts | | 21 | `2026_05_01_000001_add_contract_id_to_feedback_table` | **(NEW)** add contract_id FK to feedback | | 22 | `2026_05_01_000002_create_handovers_table` | **(NEW)** handovers | | 23 | `2026_05_01_000003_create_product_services_table` | **(NEW)** product_services | --- ## Seeder (AfterSalesSeeder) — Data mẫu - 3 users (admin, manager, staff) + Spatie roles - 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) — NEW - **5 handovers** (2 Đã BG, 1 Đủ ĐK, 1 Đã lên lịch, 1 Chưa đủ ĐK) — NEW - **5 product_services** (2 Management Fee, 2 Gratitude, 1 Self Business) — NEW - 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) ---|--- | 22 | `2026_05_01_000003_create_product_services_table` | **(NEW)** product_services | --- ## Data Import (`app:import-cskh`) — NEW 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 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 8. **Navigation group icons:** Filament 5 báo lỗi nếu cả navigation group VÀ child items đều có icons --- ## 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 | | `PROGRESS.md` | Lịch sử tiến độ hoàn thành (8 giai đoạn + bug fix) | | `ai_instructions/AI_01_*.md` | Business Logic & Policies (hợp đồng snapshot, transfer policy, handover) | | `ai_instructions/AI_02_*.md` | Database Schema Upgrade (contracts, handovers, product_services) | | `ai_instructions/AI_03_*.md` | Data Import Pipeline (spatie/simple-excel, lazy collection) | | `TASKS_ROADMAP.md` | Roadmap: việc đã làm, đang làm, sẽ làm |