24 KiB
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 | 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 |
| 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 |
| 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 ofhasRole()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
- TransferDepartment — Modal: Chọn phòng ban đích + reason (required) + attachments (optional). Visibility: admin/manager.
- 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-excelvới Lazy Collection (chống OOM) - ProgressBar + chunk 100 rows/transaction
- Pipeline mỗi dòng:
firstOrCreateProduct (Mã căn hộ) + Customer (Họ tên KH)- Insert Contract, Handover, ProductService nếu có dữ liệu
- Tách
Lịch Sử khách hàng→ Feedback + FeedbackInteraction - 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
- Form components:
Filament\Forms\Components\*(Select, TextInput, Textarea, Toggle, FileUpload, RichEditor) - Schema/layout:
Filament\Schemas\Components\*(Section, Schema) - Service access:
App::make(FileService::class)hoặc inject constructor.ActivityLogger::log()là static. - Role check:
$user->hasRole('admin')/$user->hasRole(['admin', 'manager']) - Dynamic form field: dùng
$getfunction injection, KHÔNG dùng$component->getLivewire()->data - 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
- Private disk: files lưu ở
storage/app/private/feedback-attachments/, truy cập qua signed URL từ FileService. Luôn dùngdisk('public')cho FileUpload. - Navigation group icons: Filament 5 báo lỗi nếu cả navigation group VÀ child items đều có icons
- 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) |