- PermissionSeeder: 29 permissions with role mapping (admin/manager/staff) - Policies updated to use hasPermissionTo() instead of hasRole() - ExportBackup command: JSON per-table backup with chunk processing - UserResource: CRUD users with role assignment (admin only) - RoleResource: CRUD roles with permission assignment (admin only) - UserPolicy + RolePolicy: admin-only access control - ImportCskh: detailed skip logging with color in dry-run mode - Widgets: permission-based visibility checks - Tests: 8/8 pass (21 assertions)
515 lines
22 KiB
Markdown
515 lines
22 KiB
Markdown
# 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
|
|
│ │ └── Users/ # NEW
|
|
│ │ ├── UserResource.php # Admin-only user management
|
|
│ │ ├── 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/ # NEW
|
|
│ │ ├── RoleResource.php # Admin-only role management
|
|
│ │ ├── 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/ # NEW
|
|
│ └── Commands/
|
|
│ ├── ImportCskh.php # app:import-cskh command
|
|
│ └── ExportBackup.php # app:export-backup command (JSON backup)
|
|
├── 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
|
|
|
|
> **Note:** Policies now use Spatie `hasPermissionTo()` instead of `hasRole()` for granular permission control.
|
|
|
|
| Policy | viewAny/view | create | update | delete | restore/forceDelete |
|
|
|--------|-------------|--------|--------|--------|---------------------|
|
|
| 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
|
|
|
|
```
|
|
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 + PermissionSeeder) — Data mẫu
|
|
|
|
- 3 users (admin, manager, staff) + Spatie roles
|
|
- **29 granular permissions** (PermissionSeeder) — NEW
|
|
- 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)
|
|
|
|
---|---
|
|
| 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 |
|