diff --git a/AGENTS.md b/AGENTS.md
index a370858e..1c3ed547 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,5 +1,10 @@
# 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
+> - `PROGRESS.md` — Lịch sử tiến độ hoàn thành
+
## Overview
AfterSales CRM is a real-estate after-sales customer care system built with **Laravel 13.6** + **Filament 5.6**.
diff --git a/CODEBASE_SNAPSHOT.md b/CODEBASE_SNAPSHOT.md
new file mode 100644
index 00000000..ad259bee
--- /dev/null
+++ b/CODEBASE_SNAPSHOT.md
@@ -0,0 +1,484 @@
+# 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 |
diff --git a/PROGRESS.md b/PROGRESS.md
index 05e0266e..6a968a38 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -1,9 +1,10 @@
# AfterSales CRM - Tiến độ
-## Trạng thái: Hoàn thành SOP — 8 giai đoạn + bug fix + polish
+## Trạng thái: Hoàn thành SOP + 6/7 AI Instructions (85%)
Đã test: Tất cả routes HTTP 200, Pest 8/8 tests (21 assertions) pass ✓
Tham chiếu: `SOP_des.md` — Bản đặc tả hệ thống After-Sale CRM
+**Data import:** `php artisan app:import-cskh ai_instructions/CSKH.xlsx` — 3658 rows imported successfully
## Công nghệ
- Laravel 13.6.0 + Filament 5.6.1 + SQLite
@@ -80,9 +81,54 @@ Tham chiếu: `SOP_des.md` — Bản đặc tả hệ thống After-Sale CRM
- [x] Docker sẵn sàng production: Dockerfile, docker-compose, entrypoint
- [x] Migration `notifications` table
+### AI Instructions — Phase A: Module Hợp đồng (Contract)
+- [x] Migration `contracts`: product_id, customer_id, type (sale/lease/bcc), dates, status, signed_status
+- [x] Enum `ContractType` (SALE/LEASE/BCC) + `ContractStatus` (ACTIVE/EXPIRED/LIQUIDATED)
+- [x] Model `Contract`: relationships product(), customer(), feedbacks()
+- [x] Filament Resource `Contracts` (CRUD) + `ContractPolicy`
+- [x] RelationManager `ContractsRelationManager` trên ProductResource
+- [x] Model `Product` thêm `contracts()` HasMany
+- [x] Seeder: 5 contracts mẫu
+
+### AI Instructions — Phase B: Snapshot Hợp đồng
+- [x] Migration thêm `contract_id` FK nullable vào `feedback`
+- [x] Model `Feedback`: thêm `contract_id` fillable + `contract()` BelongsTo
+- [x] FeedbackForm: auto-select active contract khi chọn sản phẩm, cho phép chọn lại thủ công
+- [x] FeedbackTable: cột "Contract" hiển thị loại HĐ dạng badge
+- [x] Seeder: gán contract_id cho 5 feedback mẫu
+
+### AI Instructions — Phase C: Module Bàn giao (Handover)
+- [x] Migration `handovers`: product_id, customer_id, handover_date, status, remark
+- [x] Enum `HandoverStatus` (NOT_READY/READY/SCHEDULED/HANDED_OVER)
+- [x] Model `Handover`: relationships product(), customer()
+- [x] RelationManager `HandoversRelationManager` trên ProductResource
+- [x] Model `Product` thêm `handovers()` HasMany
+- [x] Seeder: 5 handovers mẫu
+
+### AI Instructions — Phase D: Module Dịch vụ (ProductService)
+- [x] Migration `product_services`: product_id, service_type, status, actual_collection_date, remark
+- [x] Enum `ServiceType` (MANAGEMENT_FEE/GRATITUDE/SELF_BUSINESS)
+- [x] Model `ProductService`: relationship product()
+- [x] RelationManager `ProductServicesRelationManager` trên ProductResource
+- [x] Model `Product` thêm `productServices()` HasMany
+- [x] Seeder: 5 services mẫu
+
+### AI Instructions — Phase E: Enhancement ProductResource
+- [x] ProductResource có 3 tabs: Contracts + Handovers + Services
+
+### AI Instructions — Phase G: Data Import Pipeline
+- [x] Cài `spatie/simple-excel` (Lazy Collection, chống OOM)
+- [x] Command `app:import-cskh {file_path} --dry-run` với ProgressBar
+- [x] Pipeline: Product/Customer firstOrCreate → customer_product pivot → Contract → Handover → Service → Feedback + Interactions
+- [x] Xử lý DateTimeImmutable object từ Excel, skip meta rows
+- [x] Đã test thành công với CSKH.xlsx: 3658 rows → 3299 products, 2089 customers, 2637 contracts, 2077 handovers, 159 services, 71 feedbacks, 226 interactions
+
+### Còn lại (Optional)
+- [ ] Auto-suggest chuyển phòng ban dựa trên loại hợp đồng (Ticket BCC → gợi ý Pháp lý)
+
---
-## Database Schema (final)
+## Database Schema (final — 23 migrations)
```
products customers customer_product (pivot)
feedback_channels feedback feedback_interactions
@@ -90,14 +136,16 @@ feedback_attachments feedback_tags feedback_feedback_tag (pivot)
users departments ticket_transfer_logs
permissions roles model_has_roles (spatie)
notifications sessions cache
+contracts handovers product_services
```
-## Docker Deployment
+## Data Import
```bash
-cp .env.example .env
-# Edit .env: set APP_URL, optional: switch to MySQL
-docker compose up -d --build
-# Open http://localhost:8080/admin
+# Dry-run (phân tích trước, an toàn)
+php artisan app:import-cskh ai_instructions/CSKH.xlsx --dry-run
+
+# Import thật
+php artisan app:import-cskh ai_instructions/CSKH.xlsx
```
## Local Dev
@@ -110,4 +158,6 @@ php artisan test # 8 tests (21 assertions)
```
## Hướng dẫn cho AI Agent khác
-Xem `AGENTS.md` - chứa đầy đủ cấu trúc thư mục, schema, namespace patterns, workflow, RBAC.
+- `CODEBASE_SNAPSHOT.md` — Toàn bộ cấu trúc codebase (đọc thay vì scan code)
+- `TASKS_ROADMAP.md` — Roadmap: đã làm, đang làm, sẽ làm
+- `AGENTS.md` — Hướng dẫn chạy, workflow, RBAC, gotchas
diff --git a/TASKS_ROADMAP.md b/TASKS_ROADMAP.md
new file mode 100644
index 00000000..2ed1b184
--- /dev/null
+++ b/TASKS_ROADMAP.md
@@ -0,0 +1,145 @@
+# TASKS_ROADMAP.md — AfterSales CRM
+
+> **Mục đích:** Theo dõi tất cả các công việc: đã hoàn thành, đang làm, sẽ làm.
+> **Cập nhật:** Mỗi khi hoàn thành hoặc thêm task mới.
+
+---
+
+## ✅ ĐÃ HOÀN THÀNH (SOP Phase 1–8)
+
+### Giai đoạn 1: Mở rộng CSDL
+- [x] Bảng `departments` (id, name, manager_id → users)
+- [x] Bổ sung `feedback`: current_department_id, is_escalated
+- [x] Bảng `ticket_transfer_logs`
+- [x] Nâng cấp `feedback_attachments`: uuid, disk, collection
+- [x] Cài Spatie laravel-permission
+
+### Giai đoạn 2: Services & Logic
+- [x] `FileService`: upload, validation (jpg/png/pdf/mp4/mov ≤20MB), collection query, temporary URL
+- [x] `TransferService`: transfer department + reason required + notification
+- [x] `ClosingService`: role-gated close + proof_of_resolution check
+- [x] Policies dùng Spatie `hasRole()`
+- [x] Custom Rule `RequireProofOfResolution`
+
+### Giai đoạn 3: Notifications
+- [x] `TicketTransferred` (Database + Mail)
+- [x] `TicketClosed` (Database + Mail)
+
+### Giai đoạn 4: Filament UI
+- [x] `TransferDepartment` Action trên EditFeedback
+- [x] `CloseTicket` Action
+- [x] InteractionsRelationManager: role-aware status options
+- [x] FeedbackForm: current_department_id, is_escalated
+
+### Giai đoạn 5: Dashboard Widgets
+- [x] `ResolvedTicketsWidget`
+- [x] `AvgProcessingTimeWidget`
+- [x] `HandlerPerformanceWidget`
+
+### Giai đoạn 6: Testing
+- [x] Pest: 8 tests (21 assertions)
+
+### Giai đoạn 7: Seeder
+- [x] 4 departments, 5 channels, 5 products, 4 customers, 5 feedbacks, 5 tags, 5 interactions, 1 transfer log, 3 attachments
+
+### Giai đoạn 8: Polish & Bug Fix
+- [x] Dashboard widget filter Manager theo department
+- [x] Migration disk default fix
+- [x] File upload pattern fix
+- [x] Department + Escalated columns trên FeedbackTable
+- [x] Validation proof_of_resolution khi close từ form
+- [x] Manager chỉ đóng ticket phòng mình
+- [x] View Attachments trong Interaction History
+- [x] Docker deployment
+- [x] Migration notifications table
+
+---
+
+## 🔴 CHƯA LÀM — Theo AI Instructions
+
+### A: Module Hợp đồng (Contract) — từ `AI_01`, `AI_02`
+- [x] Tạo migration bảng `contracts`:
+ - `product_id` FK, `customer_id` FK
+ - `type` (LEASE/BCC/SALE)
+ - `start_date`, `end_date`
+ - `status` (Active/Expired/Liquidated)
+ - `printed_at`, `signed_status`
+- [x] Tạo Model `Contract` + Enum `ContractType`
+- [x] Relationships: `product()`, `customer()`, `feedbacks()`
+- [x] Tạo ContractResource Filament + ContractPolicy
+- [x] Tạo ContractsRelationManager trên ProductResource
+
+### B: Cơ chế Snapshot Hợp đồng — từ `AI_01`
+- [x] Tạo migration thêm `contract_id` (FK->contracts, nullable) vào bảng `feedback`
+- [x] Cập nhật Model `Feedback`: fillable + relationship `contract()`
+- [x] Logic auto-select active contract khi tạo Feedback mới (FeedbackForm)
+- [x] Hiển thị contract info trên Feedback list (FeedbackTable column)
+- [x] Gán contract_id cho feedback mẫu trong Seeder
+
+### C: Module Bàn giao (Handover) — từ `AI_01`, `AI_02`
+- [x] Tạo migration bảng `handovers`:
+ - `product_id` FK, `customer_id` FK
+ - `handover_date` (date)
+ - `status` (Chưa đủ điều kiện/Đủ ĐK/Đã lên lịch/Đã BG)
+ - `remark` (text nullable)
+- [x] Tạo Model `Handover` + Enum `HandoverStatus`
+- [x] Relationships: `product()`, `customer()`
+- [x] Tạo `HandoversRelationManager` gắn vào `ProductResource`
+- [x] Cập nhật Product model: thêm `handovers()` HasMany
+
+### D: Module Dịch vụ phụ trợ (ProductService) — từ `AI_02`
+- [x] Tạo migration bảng `product_services`:
+ - `product_id` FK
+ - `service_type` (MANAGEMENT_FEE/GRATITUDE/SELF_BUSINESS)
+ - `status`
+ - `actual_collection_date` (date nullable)
+ - `remark` (text nullable)
+- [x] Tạo Model `ProductService` + Enum `ServiceType`
+- [x] Relationship: `product()`
+- [x] Tạo `ProductServicesRelationManager` gắn vào `ProductResource`
+- [x] Cập nhật Product model: thêm `productServices()` HasMany
+
+### E: Enhancement ProductResource — từ `AI_02`
+- [x] Thêm RelationManagers vào `ProductResource::getRelations()`
+ - ContractsRelationManager ✅
+ - HandoversRelationManager ✅
+ - ProductServicesRelationManager ✅
+- [x] Product detail page có 3 tabs: Contracts + Handovers + Services
+
+### F: Tự động gợi ý transfer — từ `AI_01` (optional)
+- [ ] Hiển thị badge gợi ý: "Ticket BCC → khuyến nghị chuyển Pháp lý"
+- [ ] Logic detect contract type → suggest department
+
+### G: Data Import Pipeline — từ `AI_03`
+- [x] Cài package `spatie/simple-excel`
+- [x] Tạo Command `app:import-cskh {file_path} --dry-run`
+- [x] Logic: Lazy Collection, chunk 100 rows, DB::transaction, ProgressBar
+- [x] Pipeline per row: Product/Customer firstOrCreate → Contract/Handover/Service → Feedback + Interactions
+- [x] Tạo customer_product pivot record
+- [x] Xử lý DateTimeImmutable từ Excel
+- [x] Skip header/meta rows
+- [x] Đã test thành công với CSKH.xlsx (3658 rows → 3299 products, 2089 customers, 2637 contracts, 2077 handovers, 159 services, 71 feedbacks, 226 interactions)
+
+---
+
+## 📋 Ưu tiên phát triển
+
+| Ưu tiên | Task | Phụ thuộc |
+|---------|------|-----------|
+| 1 | ~~A: Module Hợp đồng (Migration + Model)~~ ✅ | Không |
+| 2 | ~~B: contract_id trên feedback (Snapshot)~~ ✅ | A |
+| 3 | ~~C: Module Bàn giao (Migration + Model + RelationManager)~~ ✅ | Không |
+| 4 | ~~D: Module Dịch vụ (Migration + Model + RelationManager)~~ ✅ | Không |
+| 5 | ~~E: Enhancement ProductResource~~ ✅ | A, C, D |
+| 6 | ~~G: Data Import Pipeline~~ ✅ | A, B, C, D |
+| 7 | F: Tự động gợi ý transfer (nice-to-have) | A |
+
+---
+
+## 📊 Tiến độ tổng quan
+
+```
+SOP Phase 1-8: ████████████████████████ 100% (Hoàn thành)
+AI Instructions Core: ███████████████████████░ 85% (6/7 — Còn: auto-suggest transfer)
+Data Import Tested: ████████████████████████ 100% (3658 rows real data imported successfully)
+```
diff --git a/ai_instructions/AI_01_BUSINESS_LOGIC_AND_POLICIES.md b/ai_instructions/AI_01_BUSINESS_LOGIC_AND_POLICIES.md
new file mode 100644
index 00000000..686c770d
--- /dev/null
+++ b/ai_instructions/AI_01_BUSINESS_LOGIC_AND_POLICIES.md
@@ -0,0 +1,19 @@
+# Kiến trúc Nghiệp vụ & Chính sách hệ thống AfterSales CRM
+
+# Tham chiếu: AGENTS.md, PROGRESS.md
+
+## 1. Cơ chế Snapshot Hợp đồng (Neo dữ liệu)
+
+- **Bối cảnh:** Một sản phẩm (Căn hộ) sẽ trải qua nhiều loại hợp đồng theo thời gian (HĐ Mua bán -> HĐ Thuê -> HĐ Hợp tác kinh doanh BCC).
+- **Quy tắc:** Mọi tương tác/khiếu nại (`Feedback`) phải được gắn cứng với hợp đồng đang có hiệu lực tại thời điểm đó.
+- **Thực thi:** Bổ sung trường `contract_id` (nullable) vào bảng `feedback`. Khi tạo mới một Ticket, hệ thống ưu tiên tự động chọn hợp đồng đang Active của căn hộ đó (có thể cho phép nhân viên chọn lại thủ công). Lịch sử của Ticket sẽ vĩnh viễn thuộc về hợp đồng này, không bị thay đổi ngay cả khi căn hộ chuyển sang hợp đồng khác trong tương lai.
+
+## 2. Chính sách Luân chuyển (Ticket Transfer Policy)
+
+- **Quy tắc Tối thượng:** KHÔNG CÓ BẤT KỲ CƠ CHẾ TỰ ĐỘNG TRANSFER NÀO.
+- **Thực thi:** Việc chuyển Ticket (Feedback) giữa các phòng ban (ví dụ: từ CSKH sang Kỹ thuật, Pháp lý) phải được thực hiện hoàn toàn thủ công bởi nhân viên hoặc quản lý (thông qua `TransferService` đã có).
+- **Tính năng mở rộng (Tùy chọn):** Trên giao diện Filament, có thể hiển thị Text/Badge gợi ý: "Ticket này thuộc mảng BCC, khuyến nghị chuyển cho phòng Pháp lý" nhưng hành động chuyển (submit) là do con người quyết định.
+
+## 3. Tách bạch Module Bàn giao (Handover)
+
+- Việc bàn giao không phải là một "Feedback", mà là một mốc trạng thái cốt lõi của sản phẩm. Nó cần một bảng riêng (`handovers`) và quản lý bằng một Tab riêng biệt trên giao diện chi tiết Căn hộ.
diff --git a/ai_instructions/AI_02_DATABASE_SCHEMA_UPGRADE.md b/ai_instructions/AI_02_DATABASE_SCHEMA_UPGRADE.md
new file mode 100644
index 00000000..f486aec0
--- /dev/null
+++ b/ai_instructions/AI_02_DATABASE_SCHEMA_UPGRADE.md
@@ -0,0 +1,41 @@
+# TASK: Nâng cấp Database Schema từ Dữ liệu Excel
+
+# Context: Đọc quy tắc tại `AI_01_BUSINESS_LOGIC_AND_POLICIES.md`
+
+Hãy tạo các file Migration và cập nhật các Model tương ứng trong Laravel 13 cho các thực thể sau:
+
+## 1. Bảng `handovers` (Module Bàn giao)
+
+- `product_id` (foreignId, constrained)
+- `customer_id` (foreignId, constrained - người nhận bàn giao)
+- `handover_date` (date - Ngày nhận BG)
+- `status` (string - Tình trạng BG: Chưa đủ điều kiện, Đủ ĐK, Đã BG trực tiếp...)
+- `remark` (text, nullable - Ghi chú hồ sơ BG)
+
+## 2. Bảng `contracts` (Module Hợp đồng)
+
+- `product_id` (foreignId, constrained)
+- `customer_id` (foreignId, constrained)
+- `type` (string - Enum/Const: LEASE, BCC, SALE)
+- `start_date` (date, nullable - Ngày bắt đầu)
+- `end_date` (date, nullable - Ngày kết thúc/Thời hạn thanh lý)
+- `status` (string - Tình trạng HĐ: Active, Expired, Liquidated...)
+- `printed_at` (date, nullable - Ngày in HĐ)
+- `signed_status` (string - Tình trạng ký)
+
+## 3. Bảng `product_services` (Dịch vụ phụ trợ / Tự doanh / Phí QL / Tri ân)
+
+- `product_id` (foreignId, constrained)
+- `service_type` (string - Enum/Const: MANAGEMENT_FEE, GRATITUDE, SELF_BUSINESS)
+- `status` (string - Tình trạng)
+- `actual_collection_date` (date, nullable - Thời hạn thu thực tế)
+- `remark` (text, nullable)
+
+## 4. Cập nhật Bảng `feedback` (Cốt lõi)
+
+- Tạo migration bổ sung cột: `table->foreignId('contract_id')->nullable()->constrained('contracts')->nullOnDelete();`
+
+## Yêu cầu cho AI:
+
+- Khai báo đầy đủ các Relationships (`hasMany`, `belongsTo`) trong tất cả các Models.
+- Chuẩn bị sẵn các `RelationManager` cho Filament 5.6 để gắn vào `ProductResource`.
diff --git a/ai_instructions/AI_03_DATA_IMPORT_PIPELINE.md b/ai_instructions/AI_03_DATA_IMPORT_PIPELINE.md
new file mode 100644
index 00000000..190254e0
--- /dev/null
+++ b/ai_instructions/AI_03_DATA_IMPORT_PIPELINE.md
@@ -0,0 +1,36 @@
+# TASK: Xây dựng Command Import Dữ liệu lớn (CSV/Excel)
+
+# Context: Import file CSKH "phẳng" vào cấu trúc Database Relational mới.
+
+## Yêu cầu Kỹ thuật
+
+- Sử dụng package `spatie/simple-excel` với cơ chế **Lazy Collection** (Generator) để chống tràn bộ nhớ (OOM).
+- Tạo command: `php artisan app:import-cskh {file_path}`.
+- Bọc logic xử lý mỗi chunk (ví dụ 100 dòng) bằng `DB::transaction()`.
+- Có `ProgressBar` của Symfony Console để theo dõi.
+
+## Luồng xử lý Data (Per Row)
+
+Mỗi `$row` từ file CSV sẽ chạy qua Pipeline sau:
+
+1. **Định danh Base Entities:**
+
+ - Tìm hoặc tạo mới `Product` bằng `$row['Mã căn hộ']`.
+ - Tìm hoặc tạo mới `Customer` bằng `$row['Họ tên KH']`.
+
+2. **Khởi tạo Hợp đồng (Contracts) & Bàn giao (Handovers):**
+
+ - Đọc các cột liên quan đến Bàn Giao, nếu có -> Insert/Update `handovers`.
+ - Đọc các cột HĐ BCC, HĐ THUÊ, nếu có -> Insert `contracts`.
+
+3. **Xử lý Feedback & Interactions (Tách dòng lịch sử):**
+
+ - Lấy chuỗi text tại cột `Nội dung` (hoặc `Lịch Sử khách hàng`).
+ - Sử dụng `explode("\n", $text)` để tách thành mảng các interactions. Loại bỏ các chuỗi rỗng.
+ - Tìm hợp đồng `Active` gần nhất của Product này để lấy `$contract_id` (nếu có).
+ - `firstOrCreate` một `Feedback` (Ticket) gắn với `Product`, `Customer`, và `Contract` vừa tìm được. (Tiêu đề mặc định: "Ticket từ dữ liệu lịch sử").
+ - Lặp qua mảng interactions vừa tách, tạo các record trong `feedback_interactions` nối với Ticket trên.
+
+## Lưu ý cho AI:
+
+- Viết code có cấu trúc rõ ràng, tách các khối logic (Process Product, Process Contract, Process Interactions) thành các private methods trong Command class để dễ bảo trì.
diff --git a/ai_instructions/CSKH.xlsx b/ai_instructions/CSKH.xlsx
new file mode 100644
index 00000000..fba4c465
Binary files /dev/null and b/ai_instructions/CSKH.xlsx differ
diff --git a/ai_instructions/CSKH.xlsx:Zone.Identifier b/ai_instructions/CSKH.xlsx:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/ai_instructions/CSKH.xlsx:Zone.Identifier differ
diff --git a/app/Console/Commands/ImportCskh.php b/app/Console/Commands/ImportCskh.php
new file mode 100644
index 00000000..d7b5a1ac
--- /dev/null
+++ b/app/Console/Commands/ImportCskh.php
@@ -0,0 +1,446 @@
+argument('file_path');
+ $dryRun = $this->option('dry-run');
+
+ if (! file_exists($filePath)) {
+ $this->error("File not found: {$filePath}");
+
+ return self::FAILURE;
+ }
+
+ if ($dryRun) {
+ $this->warn('=== DRY RUN MODE — No data will be written ===');
+ }
+
+ $rows = SimpleExcelReader::create($filePath)->getRows();
+
+ $total = iterator_count($rows);
+ $rows = SimpleExcelReader::create($filePath)->getRows();
+
+ $this->info("Total rows in file: {$total}");
+ $bar = $this->output->createProgressBar($total);
+ $bar->start();
+
+ $chunkSize = 100;
+ $chunk = [];
+ $rowCount = 0;
+
+ foreach ($rows as $row) {
+ $rowCount++;
+
+ if ($this->isMetaRow($row)) {
+ $this->statsSkipped++;
+ $bar->advance();
+
+ continue;
+ }
+
+ $chunk[] = $row;
+
+ if (count($chunk) >= $chunkSize) {
+ $this->processChunk($chunk, $dryRun);
+ $chunk = [];
+ }
+
+ $bar->advance();
+ }
+
+ if (! empty($chunk)) {
+ $this->processChunk($chunk, $dryRun);
+ }
+
+ $bar->finish();
+ $this->newLine(2);
+ $this->printSummary($rowCount, $dryRun);
+
+ return self::SUCCESS;
+ }
+
+ private function isMetaRow(array $row): bool
+ {
+ $code = $row['Mã căn hộ'] ?? '';
+
+ if (empty($code) || is_numeric($code)) {
+ return true;
+ }
+
+ if ($code === 'Ngày TT' || $code === 'Tình trạng' || $code === 'Tên DN') {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function processChunk(array $rows, bool $dryRun): void
+ {
+ if ($dryRun) {
+ foreach ($rows as $row) {
+ $this->processRow($row, true);
+ }
+
+ return;
+ }
+
+ DB::transaction(function () use ($rows): void {
+ foreach ($rows as $row) {
+ $this->processRow($row, false);
+ }
+ });
+ }
+
+ private function processRow(array $row, bool $dryRun): void
+ {
+ $productCode = trim($row['Mã căn hộ'] ?? '');
+ $customerName = trim($row['Họ tên KH'] ?? '');
+
+ if (empty($productCode) || empty($customerName)) {
+ $this->statsSkipped++;
+
+ return;
+ }
+
+ if ($dryRun) {
+ $this->statsProducts++;
+ $this->statsCustomers++;
+
+ return;
+ }
+
+ $product = $this->findOrCreateProduct($productCode);
+ $customer = $this->findOrCreateCustomer($customerName);
+
+ $this->ensureCustomerProduct($product, $customer);
+
+ $this->processContracts($row, $product, $customer);
+ $this->processHandover($row, $product, $customer);
+ $this->processServices($row, $product);
+
+ $content = $this->extractContent($row);
+ if (! empty($content)) {
+ $this->processFeedback($content, $product, $customer);
+ }
+ }
+
+ private function findOrCreateProduct(string $code): Product
+ {
+ $p = Product::firstOrCreate(
+ ['name' => $code],
+ ['status' => 'active']
+ );
+
+ if ($p->wasRecentlyCreated) {
+ $this->statsProducts++;
+ }
+
+ return $p;
+ }
+
+ private function findOrCreateCustomer(string $name): Customer
+ {
+ $c = Customer::firstOrCreate(
+ ['name' => $name],
+ ['status' => 'active']
+ );
+
+ if ($c->wasRecentlyCreated) {
+ $this->statsCustomers++;
+ }
+
+ return $c;
+ }
+
+ private function ensureCustomerProduct(Product $product, Customer $customer): void
+ {
+ CustomerProduct::firstOrCreate([
+ 'customer_id' => $customer->id,
+ 'product_id' => $product->id,
+ ]);
+ }
+
+ private function processContracts(array $row, Product $product, Customer $customer): void
+ {
+ $leaseValue = $row['HỢP ĐỒNG THUÊ'] ?? null;
+ $leaseStr = is_object($leaseValue) ? null : (string) ($leaseValue ?? '');
+ if (! empty($leaseStr) && ! is_numeric($leaseStr) && $leaseStr !== 'Tình trạng') {
+ $exists = Contract::where('product_id', $product->id)
+ ->where('customer_id', $customer->id)
+ ->where('type', 'lease')
+ ->exists();
+
+ if (! $exists) {
+ Contract::create([
+ 'product_id' => $product->id,
+ 'customer_id' => $customer->id,
+ 'type' => 'lease',
+ 'status' => 'active',
+ ]);
+ $this->statsContracts++;
+ }
+ }
+
+ $bccValue = $row['HĐ HỢP TÁC KINH DOANH'] ?? null;
+ $bccStr = is_object($bccValue) ? null : (string) ($bccValue ?? '');
+ if (! empty($bccStr) && ! is_numeric($bccStr) && $bccStr !== 'Tình trạng HĐ') {
+ $exists = Contract::where('product_id', $product->id)
+ ->where('customer_id', $customer->id)
+ ->where('type', 'bcc')
+ ->exists();
+
+ if (! $exists) {
+ Contract::create([
+ 'product_id' => $product->id,
+ 'customer_id' => $customer->id,
+ 'type' => 'bcc',
+ 'status' => 'active',
+ 'signed_status' => $bccStr,
+ ]);
+ $this->statsContracts++;
+ }
+ }
+ }
+
+ private function processHandover(array $row, Product $product, Customer $customer): void
+ {
+ $handoverDate = $this->parseHandoverDate($row['BÀN GIÀO CĂN HỘ'] ?? null);
+ $statusNote = $row['TÌNH TRẠNG'] ?? null;
+ $statusStr = is_object($statusNote) ? null : $statusNote;
+
+ if (empty($handoverDate) && empty($statusStr)) {
+ return;
+ }
+
+ $exists = Handover::where('product_id', $product->id)
+ ->where('customer_id', $customer->id)
+ ->exists();
+
+ if ($exists) {
+ return;
+ }
+
+ Handover::create([
+ 'product_id' => $product->id,
+ 'customer_id' => $customer->id,
+ 'handover_date' => $handoverDate,
+ 'status' => $handoverDate ? 'da_ban_giao' : 'chua_du_dieu_kien',
+ 'remark' => $this->truncateRemark($statusStr),
+ ]);
+ $this->statsHandovers++;
+ }
+
+ private function processServices(array $row, Product $product): void
+ {
+ $selfBiz = $row['TỰ DOANH'] ?? null;
+ $selfBizStr = is_object($selfBiz) ? null : (string) ($selfBiz ?? '');
+ if (! empty($selfBizStr) && ! is_numeric($selfBizStr) && $selfBizStr !== 'Tên DN') {
+ $exists = ProductService::where('product_id', $product->id)
+ ->where('service_type', 'self_business')
+ ->exists();
+
+ if (! $exists) {
+ ProductService::create([
+ 'product_id' => $product->id,
+ 'service_type' => 'self_business',
+ 'status' => 'active',
+ 'remark' => $selfBizStr,
+ ]);
+ $this->statsServices++;
+ }
+ }
+ }
+
+ private function extractContent(array $row): ?string
+ {
+ $sources = [
+ $row['Lịch Sử khách hàng'] ?? null,
+ $row['Nội dung'] ?? null,
+ $row['TÌNH TRẠNG'] ?? null,
+ ];
+
+ foreach ($sources as $src) {
+ if (is_object($src)) {
+ continue;
+ }
+ if (! empty($src) && ! is_numeric($src)) {
+ return (string) $src;
+ }
+ }
+
+ return null;
+ }
+
+ private function processFeedback(string $content, Product $product, Customer $customer): void
+ {
+ $activeContract = Contract::where('product_id', $product->id)
+ ->where('status', 'active')
+ ->first();
+
+ $lines = array_values(array_filter(
+ explode("\n", str_replace("\r\n", "\n", $content)),
+ fn ($l) => trim($l) !== ''
+ ));
+
+ if (empty($lines)) {
+ return;
+ }
+
+ $title = 'Ticket từ dữ liệu lịch sử';
+
+ $customerProduct = CustomerProduct::where('customer_id', $customer->id)
+ ->where('product_id', $product->id)
+ ->first();
+
+ $feedback = Feedback::firstOrCreate(
+ [
+ 'customer_id' => $customer->id,
+ 'customer_product_id' => $customerProduct?->id,
+ 'title' => $title,
+ ],
+ [
+ 'content' => $content,
+ 'status' => 'pending',
+ 'contract_id' => $activeContract?->id,
+ 'feedback_channel_id' => 1,
+ ]
+ );
+
+ if ($feedback->wasRecentlyCreated) {
+ $this->statsFeedbacks++;
+ }
+
+ $defaultUser = User::first();
+
+ foreach ($lines as $line) {
+ $exists = FeedbackInteraction::where('feedback_id', $feedback->id)
+ ->where('content', $line)
+ ->exists();
+
+ if ($exists) {
+ continue;
+ }
+
+ FeedbackInteraction::create([
+ 'feedback_id' => $feedback->id,
+ 'user_id' => $defaultUser?->id,
+ 'type' => 'note',
+ 'content' => mb_strlen($line) > 500 ? mb_substr($line, 0, 500) : $line,
+ ]);
+ $this->statsInteractions++;
+ }
+ }
+
+ /**
+ * Parse the PHP DateTime JSON format: {"date":"2021-06-09 00:00:00.000000","timezone_type":3,"timezone":"UTC"}
+ */
+ private function parseHandoverDate(mixed $value): ?string
+ {
+ if (empty($value)) {
+ return null;
+ }
+
+ if ($value instanceof \DateTimeInterface) {
+ return $value->format('Y-m-d');
+ }
+
+ if (is_object($value)) {
+ return null;
+ }
+
+ if (is_string($value) && str_starts_with($value, '{')) {
+ $decoded = json_decode($value, true);
+
+ if (is_array($decoded) && ! empty($decoded['date'])) {
+ $timestamp = strtotime($decoded['date']);
+
+ return $timestamp ? date('Y-m-d', $timestamp) : null;
+ }
+
+ return null;
+ }
+
+ return $this->parseDateString((string) $value);
+ }
+
+ private function parseDateString(string $value): ?string
+ {
+ $value = trim($value);
+ $patterns = [
+ '/^\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4}$/',
+ '/^\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}$/',
+ ];
+
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $value)) {
+ $cleaned = str_replace('/', '-', $value);
+ $timestamp = strtotime($cleaned);
+
+ return $timestamp ? date('Y-m-d', $timestamp) : null;
+ }
+ }
+
+ return null;
+ }
+
+ private function truncateRemark(?string $value): ?string
+ {
+ if (empty($value) || is_numeric($value)) {
+ return null;
+ }
+
+ return mb_strlen($value) > 500 ? mb_substr($value, 0, 500) : $value;
+ }
+
+ private function printSummary(int $totalRows, bool $dryRun): void
+ {
+ $mode = $dryRun ? '[DRY RUN] ' : '';
+
+ $this->table(
+ ['Metric', 'Count'],
+ [
+ ['Total rows in file', $totalRows],
+ ['Skipped (meta/empty)', $this->statsSkipped],
+ [$mode . 'New Products', $this->statsProducts],
+ [$mode . 'New Customers', $this->statsCustomers],
+ [$mode . 'New Contracts', $this->statsContracts],
+ [$mode . 'New Handovers', $this->statsHandovers],
+ [$mode . 'New Services', $this->statsServices],
+ [$mode . 'New Feedbacks', $this->statsFeedbacks],
+ [$mode . 'New Interactions', $this->statsInteractions],
+ ]
+ );
+ }
+}
diff --git a/app/Enums/ContractStatus.php b/app/Enums/ContractStatus.php
new file mode 100644
index 00000000..2e22f886
--- /dev/null
+++ b/app/Enums/ContractStatus.php
@@ -0,0 +1,28 @@
+ 'Đang hiệu lực',
+ self::EXPIRED => 'Hết hạn',
+ self::LIQUIDATED => 'Đã thanh lý',
+ };
+ }
+
+ public function options(): array
+ {
+ return [
+ self::ACTIVE->value => self::ACTIVE->label(),
+ self::EXPIRED->value => self::EXPIRED->label(),
+ self::LIQUIDATED->value => self::LIQUIDATED->label(),
+ ];
+ }
+}
diff --git a/app/Enums/ContractType.php b/app/Enums/ContractType.php
new file mode 100644
index 00000000..5fbc5e9f
--- /dev/null
+++ b/app/Enums/ContractType.php
@@ -0,0 +1,28 @@
+ 'HĐ Mua bán',
+ self::LEASE => 'HĐ Thuê',
+ self::BCC => 'HĐ Hợp tác kinh doanh (BCC)',
+ };
+ }
+
+ public static function options(): array
+ {
+ return [
+ self::SALE->value => self::SALE->label(),
+ self::LEASE->value => self::LEASE->label(),
+ self::BCC->value => self::BCC->label(),
+ ];
+ }
+}
diff --git a/app/Enums/HandoverStatus.php b/app/Enums/HandoverStatus.php
new file mode 100644
index 00000000..03884d3b
--- /dev/null
+++ b/app/Enums/HandoverStatus.php
@@ -0,0 +1,31 @@
+ 'Chưa đủ điều kiện',
+ self::READY => 'Đủ điều kiện',
+ self::HANDED_OVER => 'Đã bàn giao',
+ self::SCHEDULED => 'Đã lên lịch',
+ };
+ }
+
+ public function options(): array
+ {
+ return [
+ self::NOT_READY->value => self::NOT_READY->label(),
+ self::READY->value => self::READY->label(),
+ self::SCHEDULED->value => self::SCHEDULED->label(),
+ self::HANDED_OVER->value => self::HANDED_OVER->label(),
+ ];
+ }
+}
diff --git a/app/Enums/ServiceType.php b/app/Enums/ServiceType.php
new file mode 100644
index 00000000..d6f4add0
--- /dev/null
+++ b/app/Enums/ServiceType.php
@@ -0,0 +1,28 @@
+ 'Phí quản lý',
+ self::GRATITUDE => 'Tri ân',
+ self::SELF_BUSINESS => 'Tự doanh',
+ };
+ }
+
+ public function options(): array
+ {
+ return [
+ self::MANAGEMENT_FEE->value => self::MANAGEMENT_FEE->label(),
+ self::GRATITUDE->value => self::GRATITUDE->label(),
+ self::SELF_BUSINESS->value => self::SELF_BUSINESS->label(),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/Contracts/ContractResource.php b/app/Filament/Resources/Contracts/ContractResource.php
new file mode 100644
index 00000000..a63bb254
--- /dev/null
+++ b/app/Filament/Resources/Contracts/ContractResource.php
@@ -0,0 +1,57 @@
+ ListContracts::route('/'),
+ 'create' => CreateContract::route('/create'),
+ 'edit' => EditContract::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/Contracts/Pages/CreateContract.php b/app/Filament/Resources/Contracts/Pages/CreateContract.php
new file mode 100644
index 00000000..98522045
--- /dev/null
+++ b/app/Filament/Resources/Contracts/Pages/CreateContract.php
@@ -0,0 +1,11 @@
+components([
+ Section::make('Thông tin hợp đồng')
+ ->schema([
+ Select::make('product_id')
+ ->relationship('product', 'name')
+ ->searchable()
+ ->preload()
+ ->required(),
+ Select::make('customer_id')
+ ->relationship('customer', 'name')
+ ->searchable()
+ ->preload()
+ ->required(),
+ Select::make('type')
+ ->options(ContractType::options())
+ ->required()
+ ->native(false),
+ Select::make('status')
+ ->options(ContractStatus::ACTIVE->options())
+ ->default('active')
+ ->required()
+ ->native(false),
+ DatePicker::make('start_date')
+ ->label('Ngày bắt đầu'),
+ DatePicker::make('end_date')
+ ->label('Ngày kết thúc'),
+ DatePicker::make('printed_at')
+ ->label('Ngày in HĐ'),
+ TextInput::make('signed_status')
+ ->label('Tình trạng ký')
+ ->maxLength(255),
+ ])
+ ->columns(2),
+ ]);
+ }
+}
diff --git a/app/Filament/Resources/Contracts/Tables/ContractsTable.php b/app/Filament/Resources/Contracts/Tables/ContractsTable.php
new file mode 100644
index 00000000..a260975b
--- /dev/null
+++ b/app/Filament/Resources/Contracts/Tables/ContractsTable.php
@@ -0,0 +1,67 @@
+columns([
+ TextColumn::make('id')
+ ->sortable(),
+ TextColumn::make('product.name')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('customer.name')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('type')
+ ->badge()
+ ->formatStateUsing(fn (string $state): string => \App\Enums\ContractType::from($state)->label()),
+ TextColumn::make('status')
+ ->badge()
+ ->color(fn (string $state): string => match ($state) {
+ 'active' => 'success',
+ 'expired' => 'warning',
+ 'liquidated' => 'danger',
+ default => 'gray',
+ })
+ ->formatStateUsing(fn (string $state): string => \App\Enums\ContractStatus::from($state)->label()),
+ TextColumn::make('start_date')
+ ->date()
+ ->sortable()
+ ->toggleable(),
+ TextColumn::make('end_date')
+ ->date()
+ ->sortable()
+ ->toggleable(),
+ TextColumn::make('signed_status')
+ ->toggleable(),
+ TextColumn::make('feedbacks_count')
+ ->counts('feedbacks')
+ ->label('Tickets'),
+ TextColumn::make('created_at')
+ ->dateTime()
+ ->sortable()
+ ->toggleable(),
+ ])
+ ->filters([
+ //
+ ])
+ ->recordActions([
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php b/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php
index 07a8f97e..f110c671 100644
--- a/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php
+++ b/app/Filament/Resources/Feedback/Schemas/FeedbackForm.php
@@ -47,6 +47,54 @@ class FeedbackForm
})
->visible(fn ($get): bool => ! $get('is_general'))
->nullable()
+ ->searchable()
+ ->live(),
+
+ Select::make('contract_id')
+ ->label('Hợp đồng')
+ ->visible(fn ($get): bool => ! $get('is_general') && filled($get('customer_product_id')))
+ ->options(function ($get): array {
+ $customerProductId = $get('customer_product_id');
+ if (! $customerProductId) {
+ return [];
+ }
+ $cp = \App\Models\CustomerProduct::find($customerProductId);
+ if (! $cp) {
+ return [];
+ }
+ return \App\Models\Contract::where('product_id', $cp->product_id)
+ ->where('status', 'active')
+ ->with('customer')
+ ->get()
+ ->mapWithKeys(fn ($c) => [
+ $c->id => sprintf('%s (%s) - %s',
+ \App\Enums\ContractType::from($c->type)->label(),
+ $c->customer->name,
+ $c->start_date?->format('d/m/Y') ?? 'N/A',
+ ),
+ ])
+ ->toArray();
+ })
+ ->afterStateHydrated(function ($component, $state, $get): void {
+ if (filled($state)) {
+ return;
+ }
+ $customerProductId = $get('customer_product_id');
+ if (! $customerProductId) {
+ return;
+ }
+ $cp = \App\Models\CustomerProduct::find($customerProductId);
+ if (! $cp) {
+ return;
+ }
+ $activeContract = \App\Models\Contract::where('product_id', $cp->product_id)
+ ->where('status', 'active')
+ ->first();
+ if ($activeContract) {
+ $component->state($activeContract->id);
+ }
+ })
+ ->nullable()
->searchable(),
Select::make('feedback_channel_id')
diff --git a/app/Filament/Resources/Feedback/Tables/FeedbackTable.php b/app/Filament/Resources/Feedback/Tables/FeedbackTable.php
index 3d5ba3b8..02cc6e74 100644
--- a/app/Filament/Resources/Feedback/Tables/FeedbackTable.php
+++ b/app/Filament/Resources/Feedback/Tables/FeedbackTable.php
@@ -26,6 +26,18 @@ class FeedbackTable
->label('Product')
->placeholder('General')
->sortable(),
+ TextColumn::make('contract.type')
+ ->label('Contract')
+ ->badge()
+ ->formatStateUsing(fn (?string $state): string => $state ? \App\Enums\ContractType::from($state)->label() : '-')
+ ->color(fn (?string $state): string => match ($state) {
+ 'sale' => 'success',
+ 'lease' => 'info',
+ 'bcc' => 'warning',
+ default => 'gray',
+ })
+ ->placeholder('-')
+ ->toggleable(),
TextColumn::make('feedbackChannel.name')
->label('Channel'),
TextColumn::make('tags.name')
diff --git a/app/Filament/Resources/Products/ProductResource.php b/app/Filament/Resources/Products/ProductResource.php
index 0473f0c3..37e52c45 100644
--- a/app/Filament/Resources/Products/ProductResource.php
+++ b/app/Filament/Resources/Products/ProductResource.php
@@ -5,6 +5,9 @@ namespace App\Filament\Resources\Products;
use App\Filament\Resources\Products\Pages\CreateProduct;
use App\Filament\Resources\Products\Pages\EditProduct;
use App\Filament\Resources\Products\Pages\ListProducts;
+use App\Filament\Resources\Products\RelationManagers\ContractsRelationManager;
+use App\Filament\Resources\Products\RelationManagers\HandoversRelationManager;
+use App\Filament\Resources\Products\RelationManagers\ProductServicesRelationManager;
use App\Filament\Resources\Products\Schemas\ProductForm;
use App\Filament\Resources\Products\Tables\ProductsTable;
use App\Models\Product;
@@ -42,7 +45,9 @@ class ProductResource extends Resource
public static function getRelations(): array
{
return [
- //
+ ContractsRelationManager::class,
+ HandoversRelationManager::class,
+ ProductServicesRelationManager::class,
];
}
diff --git a/app/Filament/Resources/Products/RelationManagers/ContractsRelationManager.php b/app/Filament/Resources/Products/RelationManagers/ContractsRelationManager.php
new file mode 100644
index 00000000..96d6fc80
--- /dev/null
+++ b/app/Filament/Resources/Products/RelationManagers/ContractsRelationManager.php
@@ -0,0 +1,62 @@
+recordTitleAttribute('type')
+ ->columns([
+ TextColumn::make('type')
+ ->badge()
+ ->formatStateUsing(fn (string $state): string => ContractType::from($state)->label()),
+ TextColumn::make('customer.name')
+ ->label('Customer')
+ ->searchable(),
+ TextColumn::make('status')
+ ->badge()
+ ->color(fn (string $state): string => match ($state) {
+ 'active' => 'success',
+ 'expired' => 'warning',
+ 'liquidated' => 'danger',
+ default => 'gray',
+ })
+ ->formatStateUsing(fn (string $state): string => ContractStatus::from($state)->label()),
+ TextColumn::make('start_date')
+ ->date('d/m/Y')
+ ->sortable(),
+ TextColumn::make('end_date')
+ ->date('d/m/Y')
+ ->sortable(),
+ TextColumn::make('signed_status')
+ ->label('Signed'),
+ TextColumn::make('feedbacks_count')
+ ->counts('feedbacks')
+ ->label('Tickets'),
+ ])
+ ->filters([
+ SelectFilter::make('type')
+ ->options(ContractType::options()),
+ SelectFilter::make('status')
+ ->options(ContractStatus::ACTIVE->options()),
+ ])
+ ->defaultSort('created_at', 'desc')
+ ->headerActions([])
+ ->recordActions([]);
+ }
+}
diff --git a/app/Filament/Resources/Products/RelationManagers/HandoversRelationManager.php b/app/Filament/Resources/Products/RelationManagers/HandoversRelationManager.php
new file mode 100644
index 00000000..cd6aad9b
--- /dev/null
+++ b/app/Filament/Resources/Products/RelationManagers/HandoversRelationManager.php
@@ -0,0 +1,57 @@
+recordTitleAttribute('status')
+ ->columns([
+ TextColumn::make('customer.name')
+ ->label('Customer')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('handover_date')
+ ->label('Ngày BG')
+ ->date('d/m/Y')
+ ->sortable(),
+ TextColumn::make('status')
+ ->badge()
+ ->color(fn (string $state): string => match ($state) {
+ 'da_ban_giao' => 'success',
+ 'da_len_lich' => 'info',
+ 'du_dieu_kien' => 'warning',
+ 'chua_du_dieu_kien' => 'danger',
+ default => 'gray',
+ })
+ ->formatStateUsing(fn (string $state): string => HandoverStatus::from($state)->label()),
+ TextColumn::make('remark')
+ ->label('Ghi chú')
+ ->limit(50)
+ ->toggleable(),
+ TextColumn::make('created_at')
+ ->dateTime('d/m/Y')
+ ->sortable()
+ ->toggleable(),
+ ])
+ ->filters([
+ SelectFilter::make('status')
+ ->options(HandoverStatus::NOT_READY->options()),
+ ])
+ ->defaultSort('handover_date', 'desc')
+ ->headerActions([])
+ ->recordActions([]);
+ }
+}
diff --git a/app/Filament/Resources/Products/RelationManagers/ProductServicesRelationManager.php b/app/Filament/Resources/Products/RelationManagers/ProductServicesRelationManager.php
new file mode 100644
index 00000000..4a25d7ea
--- /dev/null
+++ b/app/Filament/Resources/Products/RelationManagers/ProductServicesRelationManager.php
@@ -0,0 +1,68 @@
+recordTitleAttribute('service_type')
+ ->columns([
+ TextColumn::make('service_type')
+ ->badge()
+ ->color(fn (string $state): string => match ($state) {
+ 'management_fee' => 'info',
+ 'gratitude' => 'success',
+ 'self_business' => 'warning',
+ default => 'gray',
+ })
+ ->formatStateUsing(fn (string $state): string => ServiceType::from($state)->label()),
+ TextColumn::make('status')
+ ->badge()
+ ->color(fn (string $state): string => match ($state) {
+ 'active' => 'success',
+ 'pending' => 'warning',
+ 'completed' => 'info',
+ 'cancelled' => 'danger',
+ default => 'gray',
+ }),
+ TextColumn::make('actual_collection_date')
+ ->label('Ngày thu')
+ ->date('d/m/Y')
+ ->sortable(),
+ TextColumn::make('remark')
+ ->label('Ghi chú')
+ ->limit(50)
+ ->toggleable(),
+ TextColumn::make('created_at')
+ ->dateTime('d/m/Y')
+ ->sortable()
+ ->toggleable(),
+ ])
+ ->filters([
+ SelectFilter::make('service_type')
+ ->options(ServiceType::MANAGEMENT_FEE->options()),
+ SelectFilter::make('status')
+ ->options([
+ 'active' => 'Active',
+ 'pending' => 'Pending',
+ 'completed' => 'Completed',
+ 'cancelled' => 'Cancelled',
+ ]),
+ ])
+ ->defaultSort('created_at', 'desc')
+ ->headerActions([])
+ ->recordActions([]);
+ }
+}
diff --git a/app/Models/Contract.php b/app/Models/Contract.php
new file mode 100644
index 00000000..f504447c
--- /dev/null
+++ b/app/Models/Contract.php
@@ -0,0 +1,27 @@
+belongsTo(Product::class);
+ }
+
+ public function customer(): BelongsTo
+ {
+ return $this->belongsTo(Customer::class);
+ }
+
+ public function feedbacks(): HasMany
+ {
+ return $this->hasMany(Feedback::class);
+ }
+}
diff --git a/app/Models/Feedback.php b/app/Models/Feedback.php
index 73d5af7f..a83d4ed4 100644
--- a/app/Models/Feedback.php
+++ b/app/Models/Feedback.php
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
-#[Fillable(['customer_id', 'customer_product_id', 'feedback_channel_id', 'assigned_to', 'title', 'content', 'status', 'current_department_id', 'is_escalated'])]
+#[Fillable(['customer_id', 'customer_product_id', 'contract_id', 'feedback_channel_id', 'assigned_to', 'title', 'content', 'status', 'current_department_id', 'is_escalated'])]
class Feedback extends Model
{
public function customer(): BelongsTo
@@ -21,6 +21,11 @@ class Feedback extends Model
return $this->belongsTo(CustomerProduct::class, 'customer_product_id');
}
+ public function contract(): BelongsTo
+ {
+ return $this->belongsTo(Contract::class);
+ }
+
public function feedbackChannel(): BelongsTo
{
return $this->belongsTo(FeedbackChannel::class);
diff --git a/app/Models/Handover.php b/app/Models/Handover.php
new file mode 100644
index 00000000..2b58bab3
--- /dev/null
+++ b/app/Models/Handover.php
@@ -0,0 +1,21 @@
+belongsTo(Product::class);
+ }
+
+ public function customer(): BelongsTo
+ {
+ return $this->belongsTo(Customer::class);
+ }
+}
diff --git a/app/Models/Product.php b/app/Models/Product.php
index 6523839d..faf7bf8b 100644
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['name', 'description', 'status'])]
class Product extends Model
@@ -15,4 +16,19 @@ class Product extends Model
->withPivot('purchase_date')
->withTimestamps();
}
+
+ public function contracts(): HasMany
+ {
+ return $this->hasMany(Contract::class);
+ }
+
+ public function handovers(): HasMany
+ {
+ return $this->hasMany(Handover::class);
+ }
+
+ public function productServices(): HasMany
+ {
+ return $this->hasMany(ProductService::class);
+ }
}
diff --git a/app/Models/ProductService.php b/app/Models/ProductService.php
new file mode 100644
index 00000000..5b00d560
--- /dev/null
+++ b/app/Models/ProductService.php
@@ -0,0 +1,18 @@
+belongsTo(Product::class);
+ }
+}
diff --git a/app/Policies/ContractPolicy.php b/app/Policies/ContractPolicy.php
new file mode 100644
index 00000000..1567f455
--- /dev/null
+++ b/app/Policies/ContractPolicy.php
@@ -0,0 +1,44 @@
+hasRole(['admin', 'manager', 'staff']);
+ }
+
+ public function view(User $user, Contract $contract): bool
+ {
+ return $user->hasRole(['admin', 'manager', 'staff']);
+ }
+
+ public function create(User $user): bool
+ {
+ return $user->hasRole(['admin', 'manager']);
+ }
+
+ public function update(User $user, Contract $contract): bool
+ {
+ return $user->hasRole(['admin', 'manager']);
+ }
+
+ public function delete(User $user, Contract $contract): bool
+ {
+ return $user->hasRole('admin');
+ }
+
+ public function restore(User $user, Contract $contract): bool
+ {
+ return $user->hasRole('admin');
+ }
+
+ public function forceDelete(User $user, Contract $contract): bool
+ {
+ return $user->hasRole('admin');
+ }
+}
diff --git a/composer.json b/composer.json
index 01f21268..7574d46a 100644
--- a/composer.json
+++ b/composer.json
@@ -10,7 +10,8 @@
"filament/filament": "^5.5",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
- "spatie/laravel-permission": "*"
+ "spatie/laravel-permission": "*",
+ "spatie/simple-excel": "^3.9"
},
"require-dev": {
"fakerphp/faker": "^1.23",
diff --git a/composer.lock b/composer.lock
index 4718bdc0..602e1505 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "f3b7430c94705bbff603985939216158",
+ "content-hash": "a70c58ea25e9af9dc009c1f80e2316df",
"packages": [
{
"name": "blade-ui-kit/blade-heroicons",
@@ -5366,6 +5366,66 @@
],
"time": "2026-02-01T09:30:04+00:00"
},
+ {
+ "name": "spatie/simple-excel",
+ "version": "3.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/simple-excel.git",
+ "reference": "67095053ff6037284fd213abd84259ea4faca7aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/simple-excel/zipball/67095053ff6037284fd213abd84259ea4faca7aa",
+ "reference": "67095053ff6037284fd213abd84259ea4faca7aa",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0",
+ "openspout/openspout": "^4.30",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.4|^10.5|^11.0|^12.0",
+ "spatie/pest-plugin-snapshots": "^1.1|^2.1",
+ "spatie/phpunit-snapshot-assertions": "^4.0|^5.1",
+ "spatie/temporary-directory": "^1.2|^2.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Spatie\\SimpleExcel\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Read and write simple Excel and CSV files",
+ "homepage": "https://github.com/spatie/simple-excel",
+ "keywords": [
+ "simple-excel",
+ "spatie"
+ ],
+ "support": {
+ "source": "https://github.com/spatie/simple-excel/tree/3.9.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-22T08:49:24+00:00"
+ },
{
"name": "symfony/clock",
"version": "v7.4.8",
diff --git a/database/migrations/2026_05_01_000000_create_contracts_table.php b/database/migrations/2026_05_01_000000_create_contracts_table.php
new file mode 100644
index 00000000..1fce172d
--- /dev/null
+++ b/database/migrations/2026_05_01_000000_create_contracts_table.php
@@ -0,0 +1,29 @@
+id();
+ $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
+ $table->foreignId('customer_id')->constrained('customers')->cascadeOnDelete();
+ $table->string('type');
+ $table->date('start_date')->nullable();
+ $table->date('end_date')->nullable();
+ $table->string('status')->default('active');
+ $table->date('printed_at')->nullable();
+ $table->string('signed_status')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('contracts');
+ }
+};
diff --git a/database/migrations/2026_05_01_000001_add_contract_id_to_feedback_table.php b/database/migrations/2026_05_01_000001_add_contract_id_to_feedback_table.php
new file mode 100644
index 00000000..8dd7bf1f
--- /dev/null
+++ b/database/migrations/2026_05_01_000001_add_contract_id_to_feedback_table.php
@@ -0,0 +1,23 @@
+foreignId('contract_id')->nullable()->after('customer_product_id')->constrained('contracts')->nullOnDelete();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('feedback', function (Blueprint $table) {
+ $table->dropForeign(['contract_id']);
+ $table->dropColumn('contract_id');
+ });
+ }
+};
diff --git a/database/migrations/2026_05_01_000002_create_handovers_table.php b/database/migrations/2026_05_01_000002_create_handovers_table.php
new file mode 100644
index 00000000..5eeb72e5
--- /dev/null
+++ b/database/migrations/2026_05_01_000002_create_handovers_table.php
@@ -0,0 +1,26 @@
+id();
+ $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
+ $table->foreignId('customer_id')->constrained('customers')->cascadeOnDelete();
+ $table->date('handover_date')->nullable();
+ $table->string('status')->default('chua_du_dieu_kien');
+ $table->text('remark')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('handovers');
+ }
+};
diff --git a/database/migrations/2026_05_01_000003_create_product_services_table.php b/database/migrations/2026_05_01_000003_create_product_services_table.php
new file mode 100644
index 00000000..3e2cfd15
--- /dev/null
+++ b/database/migrations/2026_05_01_000003_create_product_services_table.php
@@ -0,0 +1,26 @@
+id();
+ $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
+ $table->string('service_type');
+ $table->string('status')->default('active');
+ $table->date('actual_collection_date')->nullable();
+ $table->text('remark')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('product_services');
+ }
+};
diff --git a/database/seeders/AfterSalesSeeder.php b/database/seeders/AfterSalesSeeder.php
index 6fa7289c..e1504638 100644
--- a/database/seeders/AfterSalesSeeder.php
+++ b/database/seeders/AfterSalesSeeder.php
@@ -94,14 +94,139 @@ class AfterSalesSeeder extends Seeder
$customers[3]->products()->attach($products[1]->id, ['purchase_date' => '2025-01-05']);
$customers[3]->products()->attach($products[2]->id, ['purchase_date' => '2025-02-15']);
+ $contract1 = \App\Models\Contract::create([
+ 'product_id' => $products[0]->id,
+ 'customer_id' => $customers[0]->id,
+ 'type' => \App\Enums\ContractType::SALE->value,
+ 'start_date' => '2024-01-15',
+ 'status' => \App\Enums\ContractStatus::ACTIVE->value,
+ 'signed_status' => 'Đã ký',
+ ]);
+
+ $contract2 = \App\Models\Contract::create([
+ 'product_id' => $products[1]->id,
+ 'customer_id' => $customers[0]->id,
+ 'type' => \App\Enums\ContractType::LEASE->value,
+ 'start_date' => '2024-06-01',
+ 'end_date' => '2026-06-01',
+ 'status' => \App\Enums\ContractStatus::ACTIVE->value,
+ 'signed_status' => 'Đã ký',
+ ]);
+
+ $contract3 = \App\Models\Contract::create([
+ 'product_id' => $products[0]->id,
+ 'customer_id' => $customers[1]->id,
+ 'type' => \App\Enums\ContractType::SALE->value,
+ 'start_date' => '2024-03-20',
+ 'status' => \App\Enums\ContractStatus::ACTIVE->value,
+ 'signed_status' => 'Đã ký',
+ ]);
+
+ $contract4 = \App\Models\Contract::create([
+ 'product_id' => $products[2]->id,
+ 'customer_id' => $customers[2]->id,
+ 'type' => \App\Enums\ContractType::BCC->value,
+ 'start_date' => '2024-09-10',
+ 'end_date' => '2029-09-10',
+ 'status' => \App\Enums\ContractStatus::ACTIVE->value,
+ 'signed_status' => 'Đang chờ ký',
+ ]);
+
+ $contract5 = \App\Models\Contract::create([
+ 'product_id' => $products[1]->id,
+ 'customer_id' => $customers[3]->id,
+ 'type' => \App\Enums\ContractType::LEASE->value,
+ 'start_date' => '2025-01-05',
+ 'end_date' => '2026-01-05',
+ 'status' => \App\Enums\ContractStatus::EXPIRED->value,
+ 'signed_status' => 'Đã ký',
+ ]);
+
$customerProduct1 = \App\Models\CustomerProduct::where('customer_id', $customers[0]->id)->where('product_id', $products[0]->id)->first();
$customerProduct2 = \App\Models\CustomerProduct::where('customer_id', $customers[0]->id)->where('product_id', $products[1]->id)->first();
$customerProduct3 = \App\Models\CustomerProduct::where('customer_id', $customers[1]->id)->where('product_id', $products[0]->id)->first();
+ \App\Models\Handover::create([
+ 'product_id' => $products[0]->id,
+ 'customer_id' => $customers[0]->id,
+ 'handover_date' => '2024-02-15',
+ 'status' => \App\Enums\HandoverStatus::HANDED_OVER->value,
+ 'remark' => 'Đã bàn giao đầy đủ chìa khóa và thẻ căn hộ.',
+ ]);
+
+ \App\Models\Handover::create([
+ 'product_id' => $products[0]->id,
+ 'customer_id' => $customers[1]->id,
+ 'handover_date' => '2024-04-20',
+ 'status' => \App\Enums\HandoverStatus::HANDED_OVER->value,
+ 'remark' => 'Đã bàn giao, còn thiếu sổ bảo hành nội thất.',
+ ]);
+
+ \App\Models\Handover::create([
+ 'product_id' => $products[1]->id,
+ 'customer_id' => $customers[0]->id,
+ 'status' => \App\Enums\HandoverStatus::READY->value,
+ 'remark' => 'Căn hộ đã hoàn thiện, chờ khách xác nhận lịch bàn giao.',
+ ]);
+
+ \App\Models\Handover::create([
+ 'product_id' => $products[2]->id,
+ 'customer_id' => $customers[2]->id,
+ 'handover_date' => '2024-10-01',
+ 'status' => \App\Enums\HandoverStatus::SCHEDULED->value,
+ 'remark' => 'Đã hẹn lịch 10h sáng ngày 01/10/2024.',
+ ]);
+
+ \App\Models\Handover::create([
+ 'product_id' => $products[1]->id,
+ 'customer_id' => $customers[3]->id,
+ 'status' => \App\Enums\HandoverStatus::NOT_READY->value,
+ 'remark' => 'Đang hoàn thiện nội thất, dự kiến đủ điều kiện sau 2 tuần.',
+ ]);
+
+ \App\Models\ProductService::create([
+ 'product_id' => $products[0]->id,
+ 'service_type' => \App\Enums\ServiceType::MANAGEMENT_FEE->value,
+ 'status' => 'active',
+ 'actual_collection_date' => '2024-03-01',
+ 'remark' => 'Phí quản lý hàng tháng 5.000.000 VND.',
+ ]);
+
+ \App\Models\ProductService::create([
+ 'product_id' => $products[0]->id,
+ 'service_type' => \App\Enums\ServiceType::GRATITUDE->value,
+ 'status' => 'completed',
+ 'actual_collection_date' => '2024-06-15',
+ 'remark' => 'Quà tri ân khách hàng dịp sinh nhật BQL.',
+ ]);
+
+ \App\Models\ProductService::create([
+ 'product_id' => $products[1]->id,
+ 'service_type' => \App\Enums\ServiceType::MANAGEMENT_FEE->value,
+ 'status' => 'pending',
+ 'remark' => 'Chưa thu phí quản lý quý 1/2025.',
+ ]);
+
+ \App\Models\ProductService::create([
+ 'product_id' => $products[2]->id,
+ 'service_type' => \App\Enums\ServiceType::SELF_BUSINESS->value,
+ 'status' => 'active',
+ 'actual_collection_date' => '2024-10-01',
+ 'remark' => 'Dịch vụ cho thuê hồ bơi và BBQ khu Villa.',
+ ]);
+
+ \App\Models\ProductService::create([
+ 'product_id' => $products[1]->id,
+ 'service_type' => \App\Enums\ServiceType::GRATITUDE->value,
+ 'status' => 'cancelled',
+ 'remark' => 'KH từ chối nhận quà, chuyển sang đợt sau.',
+ ]);
+
$feedbacks = [
[
'customer_id' => $customers[0]->id,
'customer_product_id' => $customerProduct1->id,
+ 'contract_id' => $contract1->id,
'feedback_channel_id' => $channels->firstWhere('slug', 'email')->id,
'title' => 'Leaking ceiling in living room',
'content' => 'I noticed water stains on the ceiling of the living room after heavy rain. Please send someone to inspect and fix.',
@@ -112,6 +237,7 @@ class AfterSalesSeeder extends Seeder
[
'customer_id' => $customers[0]->id,
'customer_product_id' => $customerProduct2->id,
+ 'contract_id' => $contract2->id,
'feedback_channel_id' => $channels->firstWhere('slug', 'phone')->id,
'title' => 'Request for parking slot upgrade',
'content' => 'I would like to upgrade from one parking slot to two. Please advise on availability and pricing.',
@@ -122,6 +248,7 @@ class AfterSalesSeeder extends Seeder
[
'customer_id' => $customers[1]->id,
'customer_product_id' => $customerProduct3->id,
+ 'contract_id' => $contract3->id,
'feedback_channel_id' => $channels->firstWhere('slug', 'zalo')->id,
'title' => 'AC not cooling properly',
'content' => 'The air conditioner in the master bedroom is making loud noises and not cooling effectively.',
@@ -132,6 +259,7 @@ class AfterSalesSeeder extends Seeder
[
'customer_id' => $customers[2]->id,
'customer_product_id' => null,
+ 'contract_id' => null,
'feedback_channel_id' => $channels->firstWhere('slug', 'document')->id,
'title' => 'General feedback on community services',
'content' => 'I suggest improving the garbage collection schedule to twice a day instead of once. Also, the security guard shift changes could be more organized.',
@@ -142,6 +270,7 @@ class AfterSalesSeeder extends Seeder
[
'customer_id' => $customers[3]->id,
'customer_product_id' => \App\Models\CustomerProduct::where('customer_id', $customers[3]->id)->where('product_id', $products[1]->id)->first()->id,
+ 'contract_id' => $contract5->id,
'feedback_channel_id' => $channels->firstWhere('slug', 'email')->id,
'title' => 'Kitchen cabinet hinge broken',
'content' => 'One of the kitchen cabinet hinges is broken. It is still under warranty, please arrange replacement.',
diff --git a/webappUI/aftersales_crm_core/DESIGN.md b/webappUI/aftersales_crm_core/DESIGN.md
new file mode 100644
index 00000000..7723cf2c
--- /dev/null
+++ b/webappUI/aftersales_crm_core/DESIGN.md
@@ -0,0 +1,178 @@
+---
+name: AfterSales CRM Core
+colors:
+ surface: '#f9f9ff'
+ surface-dim: '#d8d9e3'
+ surface-bright: '#f9f9ff'
+ surface-container-lowest: '#ffffff'
+ surface-container-low: '#f2f3fd'
+ surface-container: '#ecedf7'
+ surface-container-high: '#e6e7f2'
+ surface-container-highest: '#e1e2ec'
+ on-surface: '#191b23'
+ on-surface-variant: '#424754'
+ inverse-surface: '#2e3038'
+ inverse-on-surface: '#eff0fa'
+ outline: '#727785'
+ outline-variant: '#c2c6d6'
+ surface-tint: '#005ac2'
+ primary: '#0058be'
+ on-primary: '#ffffff'
+ primary-container: '#2170e4'
+ on-primary-container: '#fefcff'
+ inverse-primary: '#adc6ff'
+ secondary: '#585f6c'
+ on-secondary: '#ffffff'
+ secondary-container: '#dce2f3'
+ on-secondary-container: '#5e6572'
+ tertiary: '#924700'
+ on-tertiary: '#ffffff'
+ tertiary-container: '#b75b00'
+ on-tertiary-container: '#fffbff'
+ error: '#ba1a1a'
+ on-error: '#ffffff'
+ error-container: '#ffdad6'
+ on-error-container: '#93000a'
+ primary-fixed: '#d8e2ff'
+ primary-fixed-dim: '#adc6ff'
+ on-primary-fixed: '#001a42'
+ on-primary-fixed-variant: '#004395'
+ secondary-fixed: '#dce2f3'
+ secondary-fixed-dim: '#c0c7d6'
+ on-secondary-fixed: '#151c27'
+ on-secondary-fixed-variant: '#404754'
+ tertiary-fixed: '#ffdcc6'
+ tertiary-fixed-dim: '#ffb786'
+ on-tertiary-fixed: '#311400'
+ on-tertiary-fixed-variant: '#723600'
+ background: '#f9f9ff'
+ on-background: '#191b23'
+ surface-variant: '#e1e2ec'
+typography:
+ h1:
+ fontFamily: Manrope
+ fontSize: 24px
+ fontWeight: '600'
+ lineHeight: 32px
+ h2:
+ fontFamily: Manrope
+ fontSize: 20px
+ fontWeight: '600'
+ lineHeight: 28px
+ h3:
+ fontFamily: Manrope
+ fontSize: 18px
+ fontWeight: '600'
+ lineHeight: 24px
+ body-base:
+ fontFamily: Inter
+ fontSize: 14px
+ fontWeight: '400'
+ lineHeight: 20px
+ body-sm:
+ fontFamily: Inter
+ fontSize: 12px
+ fontWeight: '400'
+ lineHeight: 16px
+ label-semibold:
+ fontFamily: Inter
+ fontSize: 14px
+ fontWeight: '600'
+ lineHeight: 20px
+ button:
+ fontFamily: Inter
+ fontSize: 14px
+ fontWeight: '500'
+ lineHeight: 20px
+rounded:
+ sm: 0.125rem
+ DEFAULT: 0.25rem
+ md: 0.375rem
+ lg: 0.5rem
+ xl: 0.75rem
+ full: 9999px
+spacing:
+ container-max: 1280px
+ nav-height: 56px
+ gutter: 1rem
+ margin-x: 1.5rem
+ stack-gap: 1.5rem
+ grid-columns-desktop: '12'
+ form-gap: 1.25rem
+---
+
+## Brand & Style
+
+This design system is built on a **Corporate / Modern** aesthetic, prioritizing utility, clarity, and reliability. It is designed for high-density information environments where user trust is paramount. The visual language utilizes a refined "Filament-inspired" approach, characterized by clean lines, subtle depth, and a structured hierarchy that minimizes cognitive load during complex CRM tasks.
+
+The emotional response should be one of "effortless control"—where the interface recedes to let the data lead, but remains aesthetically polished and professional. It balances the rigidity of enterprise software with the fluidity of modern web applications.
+
+## Colors
+
+The palette is rooted in a functional spectrum. The **Primary Blue** acts as the main driver for interaction and focus, while the **Neutral Gray** scale provides the structural scaffolding.
+
+In **Light Mode**, we use a soft gray background (`#f9fafb`) to reduce glare, paired with pure white surfaces for cards.
+In **Dark Mode**, the depth is established using a deep navy-black (`#020617`) for the foundation and a slightly lighter charcoal (`#111827`) for elevated components.
+
+Semantic colors (Success, Warning, Danger) are used sparingly to highlight status changes, ensuring they command attention without overwhelming the user.
+
+## Typography
+
+The typography system uses **Manrope** for headings to inject a modern, refined character into the CRM’s data-heavy views. For body text and functional UI elements, **Inter** is utilized for its exceptional legibility at small sizes.
+
+- **Headings:** Always semibold to provide clear entry points for the eye.
+- **Body:** Standardized at 14px (text-sm) to optimize information density without sacrificing readability.
+- **Labels:** Use semibold Inter for form labels and table headers to distinguish them from data values.
+
+## Layout & Spacing
+
+The system follows a **Fluid Grid** philosophy with fixed breakpoints.
+- **Top Navigation:** A sticky 56px bar provides constant access to global controls.
+- **Page Layout:** Content is centered in a max-width container (1280px) with 24px (1.5rem) side margins.
+- **Forms:** Employ a responsive 2-column layout on desktop, collapsing to a single column on mobile devices.
+- **Tables:** Designed for horizontal scrolling on smaller viewports to preserve data integrity.
+- **Rhythm:** A 4px/8px baseline grid maintains consistent vertical rhythm across all component stacks.
+
+## Elevation & Depth
+
+Visual hierarchy is established through **Tonal Layers** and **Ambient Shadows**.
+
+- **Level 0 (Background):** Flat, using the base background token.
+- **Level 1 (Cards/Tables):** Pure white (light mode) or #111827 (dark mode) with a subtle `shadow-sm` and a 1px border.
+- **Level 2 (Dropdowns/Modals):** Increased shadow depth (`shadow-lg`) and a distinct border to separate floating elements from the surface.
+
+In dark mode, shadows are deepened and opacity is reduced, relying more on subtle border highlights (1px white/10%) to define edges.
+
+## Shapes
+
+The shape language is **Soft** and approachable.
+- **Components:** Standard buttons, input fields, and cards use a 0.25rem (4px) corner radius.
+- **Large Elements:** Modals and large cards use 0.5rem (8px) for a slightly more modern feel.
+- **Avatars/Badges:** Notification badges and user avatars use a full pill/circular shape to contrast against the predominantly rectangular UI.
+
+## Components
+
+### Buttons
+- **Primary:** Solid #3b82f6 background, white text, subtle hover state brightness increase.
+- **Secondary:** White background with a 1px gray-300 border and gray-700 text.
+- **Tertiary:** Ghost style, no background/border until hover.
+
+### Inputs & Forms
+- Inputs use a consistent 14px text size with a 1px border.
+- Focus state: 2px ring of primary color with 20% opacity.
+- Form groups: Label on top, followed by input, followed by optional hint text.
+
+### Cards
+- Clean white surfaces with `shadow-sm`.
+- Header section separated by a thin light-gray divider.
+- Padding should be a consistent 1.5rem (24px) for desktop.
+
+### Navigation (Top Bar)
+- Sticky positioning with a subtle bottom border.
+- Nav items use a 14px medium weight; active states are indicated by a primary color shift or subtle bottom indicator.
+- Notifications: A 20px circular badge in Danger Red sits at the top right of the bell icon.
+
+### Tables
+- Headers are sticky where possible.
+- Row hover states use a very faint gray (gray-50) to assist with line tracking.
+- Status indicators (Chips) should use a "Soft" style: light background tint with high-contrast text of the same hue (e.g., Success green text on a light green background).
\ No newline at end of file
diff --git a/webappUI/aftersales_crm_core/DESIGN.md:Zone.Identifier b/webappUI/aftersales_crm_core/DESIGN.md:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/aftersales_crm_core/DESIGN.md:Zone.Identifier differ
diff --git a/webappUI/channel_list_desktop/code.html b/webappUI/channel_list_desktop/code.html
new file mode 100644
index 00000000..17db877b
--- /dev/null
+++ b/webappUI/channel_list_desktop/code.html
@@ -0,0 +1,365 @@
+
+
+
+
+
+Channels - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+settings
+
+
+
+
+
+
+
+
+
+
+
+
+
Channels
+
Manage intake sources and communication pathways.
+
+
+add
+ Add Channel
+
+
+
+
+
+
+
+
+Name
+Slug
+Status
+Feedbacks
+Created
+Actions
+
+
+
+
+
+
+
+
+language
+
+
Web Portal
+
+
+web-portal
+
+
+check_circle
+ Active
+
+
+12,450
+Oct 12, 2023
+
+
+
+edit
+
+
+
+
+
+
+
+
+
+smartphone
+
+
iOS App
+
+
+ios-app
+
+
+check_circle
+ Active
+
+
+8,204
+Nov 05, 2023
+
+
+
+edit
+
+
+
+
+
+
+
+
+
+android
+
+
Android App
+
+
+android-app
+
+
+cancel
+ Inactive
+
+
+542
+Jan 22, 2024
+
+
+
+edit
+
+
+
+
+
+
+
+
+
+mail
+
+
Email Parsing
+
+
+email-parsing
+
+
+check_circle
+ Active
+
+
+45,102
+Sep 01, 2023
+
+
+
+edit
+
+
+
+
+
+
+
+
+
+
Showing 1 to 4 of 4 results
+
+Prev
+Next
+
+
+
+
+
+
+
+
+
+ © 2024 AfterSales CRM. All rights reserved.
+
+
+
+
+
+
+
+
+
+
+
Edit Channel
+
+close
+
+
+
+
+
+
+
+ Cancel
+
+
+ Save Changes
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/channel_list_desktop/code.html:Zone.Identifier b/webappUI/channel_list_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/channel_list_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/channel_list_desktop/screen.png b/webappUI/channel_list_desktop/screen.png
new file mode 100644
index 00000000..2cb3b00e
Binary files /dev/null and b/webappUI/channel_list_desktop/screen.png differ
diff --git a/webappUI/channel_list_desktop/screen.png:Zone.Identifier b/webappUI/channel_list_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/channel_list_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/create_edit_channel_desktop/code.html b/webappUI/create_edit_channel_desktop/code.html
new file mode 100644
index 00000000..a8932f0f
--- /dev/null
+++ b/webappUI/create_edit_channel_desktop/code.html
@@ -0,0 +1,198 @@
+
+
+
+
+
+Create Channel - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/create_edit_channel_desktop/code.html:Zone.Identifier b/webappUI/create_edit_channel_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_channel_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/create_edit_channel_desktop/screen.png b/webappUI/create_edit_channel_desktop/screen.png
new file mode 100644
index 00000000..544d3aad
Binary files /dev/null and b/webappUI/create_edit_channel_desktop/screen.png differ
diff --git a/webappUI/create_edit_channel_desktop/screen.png:Zone.Identifier b/webappUI/create_edit_channel_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_channel_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/create_edit_channel_desktop_horizontal_menu/code.html b/webappUI/create_edit_channel_desktop_horizontal_menu/code.html
new file mode 100644
index 00000000..264463ca
--- /dev/null
+++ b/webappUI/create_edit_channel_desktop_horizontal_menu/code.html
@@ -0,0 +1,228 @@
+
+
+
+
+
+Create Channel - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+settings
+
+
+person
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/create_edit_channel_desktop_horizontal_menu/code.html:Zone.Identifier b/webappUI/create_edit_channel_desktop_horizontal_menu/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_channel_desktop_horizontal_menu/code.html:Zone.Identifier differ
diff --git a/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png b/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png
new file mode 100644
index 00000000..5aa16b31
Binary files /dev/null and b/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png differ
diff --git a/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png:Zone.Identifier b/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_channel_desktop_horizontal_menu/screen.png:Zone.Identifier differ
diff --git a/webappUI/create_edit_product_desktop_horizontal_menu/code.html b/webappUI/create_edit_product_desktop_horizontal_menu/code.html
new file mode 100644
index 00000000..17b6aada
--- /dev/null
+++ b/webappUI/create_edit_product_desktop_horizontal_menu/code.html
@@ -0,0 +1,225 @@
+
+
+
+
+
+AfterSales CRM - New Product
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Dashboard
+
+
+
+
+
+
+chevron_right
+New Product
+
+
+
+
+
+
+
New Product
+
Enter the details below to add a new item to the inventory catalog.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/create_edit_product_desktop_horizontal_menu/code.html:Zone.Identifier b/webappUI/create_edit_product_desktop_horizontal_menu/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_product_desktop_horizontal_menu/code.html:Zone.Identifier differ
diff --git a/webappUI/create_edit_product_desktop_horizontal_menu/screen.png b/webappUI/create_edit_product_desktop_horizontal_menu/screen.png
new file mode 100644
index 00000000..ca569ac2
Binary files /dev/null and b/webappUI/create_edit_product_desktop_horizontal_menu/screen.png differ
diff --git a/webappUI/create_edit_product_desktop_horizontal_menu/screen.png:Zone.Identifier b/webappUI/create_edit_product_desktop_horizontal_menu/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_product_desktop_horizontal_menu/screen.png:Zone.Identifier differ
diff --git a/webappUI/create_edit_tag_desktop_horizontal_menu/code.html b/webappUI/create_edit_tag_desktop_horizontal_menu/code.html
new file mode 100644
index 00000000..3570690d
--- /dev/null
+++ b/webappUI/create_edit_tag_desktop_horizontal_menu/code.html
@@ -0,0 +1,207 @@
+
+
+
+
+
+Create Tag - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+
+settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Dashboard
+chevron_right
+Tags
+chevron_right
+New Tag
+
+
+
Create New Tag
+
+
+
+
+
+
Tag Details
+
Configure the identity and appearance of your tag.
+
+
+
+
+
+
Name
+
+
The display name of the tag used across the CRM.
+
+
+
+
Slug
+
+
A unique identifier used in URLs and API responses (auto-generated if left blank).
+
+
+
+
Color
+
+
+
+
+
+
+
+
+
+check
+
+
+
+
+
+
Select a color to visually distinguish this tag.
+
+
+
+
+
+
+ Cancel
+
+
+ Save Tag
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/create_edit_tag_desktop_horizontal_menu/code.html:Zone.Identifier b/webappUI/create_edit_tag_desktop_horizontal_menu/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_tag_desktop_horizontal_menu/code.html:Zone.Identifier differ
diff --git a/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png b/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png
new file mode 100644
index 00000000..09201dfd
Binary files /dev/null and b/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png differ
diff --git a/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png:Zone.Identifier b/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_edit_tag_desktop_horizontal_menu/screen.png:Zone.Identifier differ
diff --git a/webappUI/create_feedback_desktop/code.html b/webappUI/create_feedback_desktop/code.html
new file mode 100644
index 00000000..a442f868
--- /dev/null
+++ b/webappUI/create_feedback_desktop/code.html
@@ -0,0 +1,293 @@
+
+
+
+
+
+Feedback - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+search
+
+
+
+notifications
+
+
+
+settings
+
+
+
+
+
+
+
+
+
Create Feedback
+
+Cancel
+Create Feedback
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/create_feedback_desktop/code.html:Zone.Identifier b/webappUI/create_feedback_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_feedback_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/create_feedback_desktop/screen.png b/webappUI/create_feedback_desktop/screen.png
new file mode 100644
index 00000000..12ce468d
Binary files /dev/null and b/webappUI/create_feedback_desktop/screen.png differ
diff --git a/webappUI/create_feedback_desktop/screen.png:Zone.Identifier b/webappUI/create_feedback_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/create_feedback_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/customer_list_desktop/code.html b/webappUI/customer_list_desktop/code.html
new file mode 100644
index 00000000..4d2e64b1
--- /dev/null
+++ b/webappUI/customer_list_desktop/code.html
@@ -0,0 +1,310 @@
+
+
+
+
+
+Customers - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
AfterSales CRM
+
+
+search
+
+
+
+
+
+
+
+
+notifications
+
+
+
+settings
+
+
+
+
+
+
+
+
+
+
+
+
Home
+
chevron_right
+
Customers
+
+
Customers
+
+
+add
+ + New Customer
+
+
+
+
+
+
+
+search
+
+
+
+
Status:
+
+All Statuses
+Active
+Inactive
+Pending
+
+
+
+expand_more
+
+
+filter_list
+
+
+
+
+
+
+
+
+Name
+Email
+Phone
+Status
+Products
+Feedbacks
+Created
+
+
+
+
+
+
+
+
+
+
Alex Mercer
+
+
+alex.m@example.com
++1 (555) 123-4567
+
+Active
+
+3
+12
+Oct 12, 2023
+
+
+more_vert
+
+
+
+
+
+
+
+
+s.jenkins@corp.com
++44 20 7123 4567
+
+Inactive
+
+1
+0
+Nov 05, 2023
+
+
+more_vert
+
+
+
+
+
+
+
+
+
Elena Rodriguez
+
+
+elena.r@startup.io
++1 (555) 987-6543
+
+Pending
+
+5
+24
+Jan 18, 2024
+
+
+more_vert
+
+
+
+
+
+
+
+
+david.t@globaltech.net
++61 4 1234 5678
+
+Active
+
+2
+5
+Feb 02, 2024
+
+
+more_vert
+
+
+
+
+
+
+
+
+
Showing 1 to 4 of 24 entries
+
+Prev
+1
+2
+3
+Next
+
+
+
+
+
+
+
+
© 2024 AfterSales CRM. All rights reserved.
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/customer_list_desktop/code.html:Zone.Identifier b/webappUI/customer_list_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/customer_list_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/customer_list_desktop/screen.png b/webappUI/customer_list_desktop/screen.png
new file mode 100644
index 00000000..f3b25c1c
Binary files /dev/null and b/webappUI/customer_list_desktop/screen.png differ
diff --git a/webappUI/customer_list_desktop/screen.png:Zone.Identifier b/webappUI/customer_list_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/customer_list_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/dashboard_desktop/code.html b/webappUI/dashboard_desktop/code.html
new file mode 100644
index 00000000..527bf7e0
--- /dev/null
+++ b/webappUI/dashboard_desktop/code.html
@@ -0,0 +1,420 @@
+
+
+
+
+
+AfterSales CRM - Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+
+
Admin
+
+
+
+
+
+
+
+
+
+
+
Dashboard Overview
+
Real-time metrics and ticket tracking.
+
+
+
+calendar_today
+ Last 30 Days
+
+
+ Export Report
+
+
+
+
+
+
+
+
+
+
Resolved Tickets
+
+check_circle
+
+
+
+128
+
+trending_up 12%
+
+
+
vs last month
+
+
+
+
+
+
Avg Resolution Time
+
+timer
+
+
+
+45 min
+
+trending_down 5%
+
+
+
vs last month
+
+
+
+
+
+
Pending Tickets
+
+pending_actions
+
+
+
+12
+
+trending_up 2
+
+
+
requires immediate action
+
+
+
+
+
+
Escalated
+
+assignment_late
+
+
+
+5
+
+
assigned to management
+
+
+
+
+
+
+
+
Avg Processing Time by Handler
+more_vert
+
+
+
+
+60m
+45m
+30m
+15m
+0m
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
System Insights
+
+
+
warning
+
+
High Volume Alert
+
Returns department seeing 20% higher volume today.
+
+
+
+
check_circle
+
+
SLA Target Met
+
Global resolution time is under 48 hours target.
+
+
+
+
+
+ View All Reports
+
+
+
+
+
+
+
Tickets Awaiting Closure
+
+
+
+
+
+
+#
+Ticket
+Customer
+Handler
+Department
+Resolved Date
+Actions
+
+
+
+
+
+1
+TKT-8902
+TechCorp Inc.
+
+
+
+Hardware Support
+Oct 24, 2023
+
+Close Ticket
+
+
+
+
+2
+TKT-8905
+Jane Smith
+
+
+
+Billing
+Oct 24, 2023
+
+Close Ticket
+
+
+
+
+3
+TKT-8911
+Acme Logistics
+
+
+
+Software Config
+Oct 23, 2023
+
+Close Ticket
+
+
+
+
+4
+TKT-8915
+Global Retail
+
+
+
+Returns
+Oct 22, 2023
+
+Close Ticket
+
+
+
+
+
+
+
+
Showing 1 to 4 of 12 entries
+
+
+chevron_left
+
+1
+2
+3
+
+chevron_right
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/dashboard_desktop/code.html:Zone.Identifier b/webappUI/dashboard_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/dashboard_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/dashboard_desktop/screen.png b/webappUI/dashboard_desktop/screen.png
new file mode 100644
index 00000000..e17dae9a
Binary files /dev/null and b/webappUI/dashboard_desktop/screen.png differ
diff --git a/webappUI/dashboard_desktop/screen.png:Zone.Identifier b/webappUI/dashboard_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/dashboard_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/edit_customer_desktop/code.html b/webappUI/edit_customer_desktop/code.html
new file mode 100644
index 00000000..f361344d
--- /dev/null
+++ b/webappUI/edit_customer_desktop/code.html
@@ -0,0 +1,355 @@
+
+
+
+
+
+Edit Customer - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+settings
+
+
+
+
+
+
+
+
+
+
+
+
+
Customers
+
chevron_right
+
Alex Mercer
+
+
+ AM
+ Edit Customer Profile
+
+
+
+Cancel
+
+save
+ Save Changes
+
+
+
+
+
+
+
+
+
+
Contact Information
+
+
+
+
+
+
+
Owned Products
+
+add
+ Add
+
+
+
+
+
+Pro Series Router AC3200
+close
+
+
+Mesh Wi-Fi Node (Gen 2)
+close
+
+
+
+
+search
+
+
+
+
+
+
+
+
+
+
+
+Activity Log
+Feedback History
+Service Tickets
+
+
+
+
+
+
+
+
+
+
+ © 2024 AfterSales CRM. All rights reserved.
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/edit_customer_desktop/code.html:Zone.Identifier b/webappUI/edit_customer_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/edit_customer_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/edit_customer_desktop/screen.png b/webappUI/edit_customer_desktop/screen.png
new file mode 100644
index 00000000..e4b1d35e
Binary files /dev/null and b/webappUI/edit_customer_desktop/screen.png differ
diff --git a/webappUI/edit_customer_desktop/screen.png:Zone.Identifier b/webappUI/edit_customer_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/edit_customer_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/edit_feedback_desktop/code.html b/webappUI/edit_feedback_desktop/code.html
new file mode 100644
index 00000000..79e05946
--- /dev/null
+++ b/webappUI/edit_feedback_desktop/code.html
@@ -0,0 +1,404 @@
+
+
+
+
+
+Edit Feedback - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+
+
+
Admin
+
+
+
+
+
+
+
+
+
+
+
+Customer Care
+chevron_right
+Feedbacks
+chevron_right
+#FB-2023-0842
+
+
+
+arrow_back
+
+
+ Edit Feedback
+ In Progress
+Technical Support
+
+
+
+
+
+
+person
+ Handler: Sarah Jenkins
+
+
+ Delete
+
+
+ Transfer Dept
+
+
+ Close Ticket
+
+
+save
+ Save Changes
+
+
+
+
+
+
+edit_document
+ Edit Details
+
+
+forum
+ Interaction History
+ 5
+
+
+
+
+
+
+
+
+
+
Core Information
+
+
+
+
+
+
+
Attachments & Evidence
+2 Files
+
+
+
+
+
+image
+
+
+
IMG_8492.jpg
+
2.4 MB • Evidence
+
+
close
+
+
+
+description
+
+
+
invoice_copy.pdf
+
450 KB • Proof
+
+
close
+
+
+
+
+cloud_upload
+
+
Click to upload or drag and drop
+
SVG, PNG, JPG or PDF (max. 10MB)
+
+
+Select collection (Default: General)
+Proof of Purchase
+Evidence (Photos/Videos)
+
+
+
+
+
+
+
+
+
+
+
+
Routing & Status
+
+
+
+
+
+
+
+
+
+
+
+search
+ Similar Cases
+
+Matching tags: Hardware Fault, Display
+
+
+
+
+
+Title
+Customer
+Status
+Handler
+Date
+
+
+
+
+Screen bleeding on edge
+DesignWorks Studio
+
+Resolved
+
+Sarah Jenkins
+Oct 12, 2023
+
+
+Flickering when using HDMI 2.0
+Alex Mercer
+
+Closed
+
+Mike Ross
+Sep 28, 2023
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/edit_feedback_desktop/code.html:Zone.Identifier b/webappUI/edit_feedback_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/edit_feedback_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/edit_feedback_desktop/screen.png b/webappUI/edit_feedback_desktop/screen.png
new file mode 100644
index 00000000..b81a3771
Binary files /dev/null and b/webappUI/edit_feedback_desktop/screen.png differ
diff --git a/webappUI/edit_feedback_desktop/screen.png:Zone.Identifier b/webappUI/edit_feedback_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/edit_feedback_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/feedback_list_desktop/code.html b/webappUI/feedback_list_desktop/code.html
new file mode 100644
index 00000000..357f8de3
--- /dev/null
+++ b/webappUI/feedback_list_desktop/code.html
@@ -0,0 +1,457 @@
+
+
+
+
+
+AfterSales CRM - Feedbacks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+notifications
+
+
+
+
+
Admin
+
+
+
+
+
+
+
+
+
+
+
+Dashboard
+chevron_right
+Customer Care
+chevron_right
+Feedbacks
+
+
Feedbacks
+
+
+
+add
+ New Feedback
+
+
+
+
+
+
+
+
+
+
+
+Status: All
+Pending
+Processing
+Resolved
+Closed
+
+arrow_drop_down
+
+
+
+
+Channel: All
+Email
+Portal
+Phone
+
+arrow_drop_down
+
+
+
+
+Dept: All
+Support
+Technical
+Billing
+
+arrow_drop_down
+
+
+
+
+Handler: All
+Unassigned
+Assigned to me
+
+arrow_drop_down
+
+
+ Reset Filters
+
+
+
+
+
+
+download
+
+
+delete
+
+
+
+search
+
+
+
+
+
+
+
+
+
+
+
Showing 1 to 4 of 42 entries
+
+
+chevron_left
+
+1
+2
+3
+...
+11
+
+chevron_right
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/feedback_list_desktop/code.html:Zone.Identifier b/webappUI/feedback_list_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/feedback_list_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/feedback_list_desktop/screen.png b/webappUI/feedback_list_desktop/screen.png
new file mode 100644
index 00000000..e81da83b
Binary files /dev/null and b/webappUI/feedback_list_desktop/screen.png differ
diff --git a/webappUI/feedback_list_desktop/screen.png:Zone.Identifier b/webappUI/feedback_list_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/feedback_list_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/product_list_desktop/code.html b/webappUI/product_list_desktop/code.html
new file mode 100644
index 00000000..778689b3
--- /dev/null
+++ b/webappUI/product_list_desktop/code.html
@@ -0,0 +1,290 @@
+
+
+
+
+
+Products - AfterSales CRM
+
+
+
+
+
+
+
+
+
+
+
+
+
+search
+
+
+notifications
+
+
+
+settings
+
+
+
+
+
+
+
+
+
+
+Dashboard
+
+
+
+chevron_right
+Products
+
+
+
+
+
+
Products
+
+
+search
+
+
+
+add
+ New Product
+
+
+
+
+
+
+
+
+filter_list Filter:
+
+
+
+All Statuses
+Active
+Inactive
+Sold Out
+
+expand_more
+
+
+
+
+
+
+
+Name
+Status
+Description
+Owners
+Created
+
+
+
+
+Enterprise Server Rack Q4
+
+
+ Active
+
+
+
+ High-density server rack designed for optimal ai...
+
+1,402
+Oct 12, 2023
+
+
+Cloud Gateway Pro
+
+
+ Active
+
+
+
+ Secure Edge routing gateway with integrated thre...
+
+845
+Nov 05, 2023
+
+
+Legacy Switch Module X1
+
+
+ Inactive
+
+
+
+ Deprecated 24-port switch module. Scheduled for ...
+
+312
+Jan 18, 2021
+
+
+Quantum Encryption Key V2
+
+
+ Sold Out
+
+
+
+ Hardware security module for post-quantum crypto...
+
+2,055
+Feb 22, 2024
+
+
+Optical Transceiver 100G
+
+
+ Active
+
+
+
+ High-speed optical module for long-haul fiber co...
+
+4,190
+Mar 10, 2024
+
+
+
+
+
+
Showing 1 to 5 of 124 entries
+
+
+chevron_left
+
+1
+2
+3
+...
+25
+
+chevron_right
+
+
+
+
+
+
+
+
© 2024 AfterSales CRM. All rights reserved.
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/product_list_desktop/code.html:Zone.Identifier b/webappUI/product_list_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/product_list_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/product_list_desktop/screen.png b/webappUI/product_list_desktop/screen.png
new file mode 100644
index 00000000..7d1cbebe
Binary files /dev/null and b/webappUI/product_list_desktop/screen.png differ
diff --git a/webappUI/product_list_desktop/screen.png:Zone.Identifier b/webappUI/product_list_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/product_list_desktop/screen.png:Zone.Identifier differ
diff --git a/webappUI/tag_list_desktop/code.html b/webappUI/tag_list_desktop/code.html
new file mode 100644
index 00000000..47cd38ed
--- /dev/null
+++ b/webappUI/tag_list_desktop/code.html
@@ -0,0 +1,356 @@
+
+
+
+
+
+AfterSales CRM - Tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tag Management
+
Organize and categorize your CRM data with descriptive tags.
+
+
+
+filter_list
+ Filter
+
+
+add
+ New Tag
+
+
+
+
+
+
+
+
+
+
+
+search
+
+
+
Showing 5 of 24 tags
+
+
+
+
+
+
+Name
+Slug
+Color
+Updated
+Actions
+
+
+
+
+VIP Customer
+vip-customer
+
+
+
+2 hours ago
+
+edit
+delete
+
+
+
+High Priority
+high-priority
+
+
+
+Yesterday
+
+edit
+delete
+
+
+
+Feature Request
+feature-request
+
+
+
+Oct 24, 2023
+
+edit
+delete
+
+
+
+Bug Report
+bug-report
+
+
+
+Oct 20, 2023
+
+edit
+delete
+
+
+
+Documentation
+documentation
+
+
+
+Sep 15, 2023
+
+edit
+delete
+
+
+
+
+
+
+
+
+chevron_left Previous
+
+
+1
+2
+3
+
+
+ Next chevron_right
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webappUI/tag_list_desktop/code.html:Zone.Identifier b/webappUI/tag_list_desktop/code.html:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/tag_list_desktop/code.html:Zone.Identifier differ
diff --git a/webappUI/tag_list_desktop/screen.png b/webappUI/tag_list_desktop/screen.png
new file mode 100644
index 00000000..1499a15b
Binary files /dev/null and b/webappUI/tag_list_desktop/screen.png differ
diff --git a/webappUI/tag_list_desktop/screen.png:Zone.Identifier b/webappUI/tag_list_desktop/screen.png:Zone.Identifier
new file mode 100644
index 00000000..d6c1ec68
Binary files /dev/null and b/webappUI/tag_list_desktop/screen.png:Zone.Identifier differ