Files
minicrm/AGENTS.md
2026-05-12 09:23:05 +00:00

25 KiB

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
  • docs/I18N_GUIDE.md — Hướng dẫn đa ngôn ngữ
  • docs/ENUM_GUIDE.md — Hướng dẫn thêm Enum mới
  • luutru/ — Archive các file đã lỗi thời (tham khảo nếu cần)

File đã lỗi thời trong luutru/: PROGRESS.md, SOP_des.md, AI_01/02/03, CHANGELOG.md, README.md gốc, SYSTEM_ASSESSMENT_PROPOSALS.md

Overview

AfterSales CRM is a real-estate after-sales customer care system built with Laravel 13.6 + Filament 5.6. Hệ thống có 2 Filament Panel:

  • /admin — Admin Panel: đầy đủ tất cả modules (Contract, Handover, Service, User/Role...)
  • /support — Support Panel: rút gọn, tập trung Feedback (Feedback, Customer, Product, Channel, Tag, ActivityLog)

How to Run

cd /home/phuongtc/vibecode/minicrm
php artisan migrate:fresh --seed
php artisan serve
# Admin panel: http://localhost:8000/admin
# Support panel: http://localhost:8000/support

Test Accounts (seeded)

Role Email Password
Admin admin@minicrm.local password
Manager manager@minicrm.local password
Staff staff@minicrm.local password

Tech Stack Versions

  • PHP 8.3
  • Laravel 13.6.0 (vendor/laravel/framework)
  • Filament 5.6.1 (vendor/filament/filament)
  • Database: SQLite (database/database.sqlite)
  • Composer: /home/phuongtc/composer
  • Spatie: laravel-permission (role-based auth)
  • Testing: Pest PHP

Running Commands

cd /home/phuongtc/vibecode/minicrm
php artisan make:model Foo
php artisan make:filament-resource Foo --generate
php artisan make:filament-relation-manager ResourceName relationship titleAttribute
php artisan make:migration create_foo_table
php artisan migrate
php artisan db:seed
php artisan route:list
php artisan storage:link   # needed for file uploads
php artisan test           # run pest tests

# Export database backup (JSON format, one file per table)
php artisan app:export-backup
php artisan app:export-backup --path=/custom/path --pretty
php artisan app:export-backup --tables=feedback,customers,products

# Import data from Excel
php artisan app:import-cskh ai_instructions/CSKH.xlsx --dry-run
php artisan app:import-cskh ai_instructions/CSKH.xlsx

Dual Panel Architecture

Hệ thống có 2 Filament Panel độc lập:

Admin Panel (/admin) — AdminPanelProvider.php

Đầy đủ tất cả modules, bao gồm:

  • Feedback, Customer, Product, Contract, FeedbackChannel, FeedbackTag
  • User, Role (admin-only)
  • 3 Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget

Support Panel (/support) — SupportPanelProvider.php

Rút gọn, chỉ tập trung vào quy trình xử lý feedback:

  • Feedback, Customer, Product, FeedbackChannel, FeedbackTag, ActivityLog
  • 3 Dashboard widgets giống Admin panel
  • Không có: Contracts, Users, Roles, Handovers, ProductServices

Cả 2 panel dùng chung: Models, Services, Policies, Database, Translations.

Directory Structure

minicrm/
├── app/
│   ├── Filament/
│   │   ├── Resources/
│   │   │   ├── ActivityLogs/              # NEW: Audit trail (read-only)
│   │   │   │   ├── ActivityLogResource.php
│   │   │   │   └── Pages/ListActivityLogs.php
│   │   │   ├── Feedback/
│   │   │   │   ├── FeedbackResource.php
│   │   │   │   ├── Schemas/FeedbackForm.php
│   │   │   │   ├── Tables/FeedbackTable.php
│   │   │   │   ├── Pages/{Create,Edit,List}Feedback.php
│   │   │   │   └── RelationManagers/InteractionsRelationManager.php
│   │   │   ├── Products/
│   │   │   │   └── RelationManagers/{Contracts,Handovers,ProductServices}RelationManager.php
│   │   │   ├── Customers/
│   │   │   │   └── RelationManagers/FeedbacksRelationManager.php
│   │   │   ├── Contracts/
│   │   │   ├── FeedbackChannels/
│   │   │   ├── FeedbackTags/
│   │   │   ├── Users/
│   │   │   └── Roles/
│   │   └── Widgets/
│   │       ├── ResolvedTicketsWidget.php       # Dashboard: resolved tickets table
│   │       ├── AvgProcessingTimeWidget.php     # Dashboard: stats overview
│   │       └── HandlerPerformanceWidget.php    # Dashboard: bar chart per handler
│   ├── Console/Commands/
│   │   ├── ImportCskh.php                      # app:import-cskh
│   │   └── ExportBackup.php                    # app:export-backup
│   ├── Enums/
│   │   ├── ContractCategory.php              # V2: ownership/business
│   │   ├── ContractType.php
│   │   ├── ContractStatus.php
│   │   ├── HandoverStatus.php
│   │   └── ServiceType.php
│   ├── Http/
│   │   ├── Controllers/Controller.php
│   │   └── Middleware/HandleUploadErrors.php
│   ├── Models/
│   │   ├── ActivityLog.php                 # NEW: Audit trail model
│   │   ├── Contract.php                    # V2: +category, +representative_*
│   │   ├── Customer.php                    # V2: ownedProducts() qua contracts
│   │   ├── Department.php
│   │   ├── Feedback.php                    # V2: bỏ customer_product_id
│   │   ├── FeedbackAttachment.php
│   │   ├── FeedbackChannel.php
│   │   ├── FeedbackInteraction.php
│   │   ├── FeedbackTag.php
│   │   ├── Handover.php
│   │   ├── Product.php
│   │   ├── ProductService.php
│   │   ├── TicketTransferLog.php
│   │   └── User.php
│   ├── Policies/
│   │   ├── ActivityLogPolicy.php           # NEW
│   │   ├── ContractPolicy.php
│   │   ├── CustomerPolicy.php
│   │   ├── FeedbackChannelPolicy.php
│   │   ├── FeedbackPolicy.php
│   │   ├── ProductPolicy.php
│   │   ├── RolePolicy.php
│   │   └── UserPolicy.php
│   ├── Notifications/
│   │   ├── TicketTransferred.php
│   │   └── TicketClosed.php
│   ├── Providers/Filament/
│   │   ├── AdminPanelProvider.php          # Panel đầy đủ (/admin)
│   │   └── SupportPanelProvider.php        # Panel rút gọn (/support)
│   ├── Rules/
│   │   └── RequireProofOfResolution.php
│   └── Services/
│       ├── ActivityLogger.php              # NEW: static log() cho audit trail
│       ├── ClosingService.php
│       ├── FileService.php
│       └── TransferService.php
├── lang/                                   # i18n translation files
│   ├── en/{app.php, feedback.php, enums.php}
│   └── vi/{app.php, feedback.php, enums.php}
├── resources/views/filament/resources/feedback/
│   ├── similar-cases.blade.php
│   └── attachment-links.blade.php
├── webappUI/aftersales_crm_core/           # Design tokens & system
│   └── DESIGN.md
├── database/
│   ├── migrations/                         # 25 migrations
│   └── seeders/{AfterSalesSeeder,PermissionSeeder}.php
├── tests/Feature/
│   ├── ClosingPermissionTest.php
│   └── TransferFlowTest.php
├── luutru/                                 # Archive các file đã lỗi thời
├── UI_DESIGN_BRIEF.md                      # Thiết kế UI chi tiết
└── TASKS_ROADMAP.md

Database Schema

activity_logs (NEW)

Column Type Notes
id bigint PK
user_id fk -> users nullable Who performed action
action string import, export, transfer, close, create, update, delete
description text Human-readable description
subject_type string nullable Related model class
subject_id bigint nullable Related model ID
metadata json nullable Extra data
created_at timestamp

contracts (V2: category + representative)

Column Type Notes
id bigint PK
product_id fk -> products cascadeOnDelete
customer_id fk -> customers cascadeOnDelete
category string (V2) ownership | business
type string (V2) deposit, purchase, transfer, rental, self_business, bcc
start_date, end_date date nullable
status string active, expired, liquidated
printed_at date nullable
signed_status string nullable
representative_name string nullable (V2)
representative_phone string nullable (V2)
representative_email string nullable (V2)
created_at, updated_at timestamp

V2 Constraint: Mỗi product chỉ có tối đa 1 contract active mỗi category. Validation ở cả Model level (booted::saving) và UI.

departments (NEW)

Column Type Notes
id bigint PK
name string
manager_id fk -> users nullable Department head
created_at, updated_at timestamp

ticket_transfer_logs

Column Type Notes
id bigint PK
feedback_id fk -> feedback cascadeOnDelete
from_department_id fk -> departments nullable
to_department_id fk -> departments nullable
sender_id fk -> users Who initiated transfer
reason text Mandatory
created_at, updated_at timestamp

feedback (expanded)

Column Type Notes
id bigint PK
customer_id fk -> customers cascadeOnDelete
contract_id fk -> contracts nullable (V2) Snapshot contract (thay thế customer_product_id)
feedback_channel_id fk -> feedback_channels
assigned_to fk -> users nullable Current handler
current_department_id fk -> departments nullable Current department
is_escalated boolean default false
title string
content text
status string pending, processing, resolved, closed
created_at, updated_at timestamp

feedback_attachments (expanded)

Column Type Notes
id bigint PK
uuid string nullable unique
feedback_id fk -> feedback
feedback_interaction_id fk -> feedback_interactions nullable
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
created_at, updated_at timestamp

Other tables

feedback_interactions, feedback_tags, feedback_feedback_tag, products, customers, feedback_channels, users, contracts, handovers, product_services, roles, permissions, model_has_roles (Spatie), notifications, sessions, cache

Workflow

1. Record Feedback → Assign handler → Assign department
2. Handler contacts customer / does work
3. Handler updates → May forward to another user (assignment interaction)
4. OR: Transfer to another department → TransferDepartment Action (reason required, notification sent)
5. Loop until → Marked as "resolved" (any role) 
6. Manager reviews → Close Ticket Action (requires role=admin/manager + proof_of_resolution evidence)

Key Features

Dual Panel System (NEW)

  • Admin Panel (/admin): Đầy đủ modules, User/Role management
  • Support Panel (/support): Rút gọn, tập trung Feedback. Dùng cho đội support không cần quản lý hệ thống

ActivityLog — Audit Trail (NEW)

  • Tự động ghi log các hành động: import, export, transfer, close, create, update, delete
  • ActivityLogger::log(action, description, subject, metadata, userId)
  • Xem tại /admin/activity-logs hoặc /support/activity-logs
  • Admin-only access (ActivityLogPolicy)

Transfer Department (SOP compliant)

  • Action trên EditFeedback: TransferDepartment
  • Modal: select target department + reason (required) + optional attachments
  • Calls TransferService: creates TicketTransferLog, updates current_department_id, resets handler to dept manager
  • Sends TicketTransferred notification to target department manager

Close Ticket (SOP compliant)

  • Action trên EditFeedback: CloseTicket
  • Visible only to admin/manager, only when status != 'closed'
  • Requires confirmation
  • Staff (403) if tries to close
  • 422 if no proof_of_resolution attachment exists
  • Sends TicketClosed notification to handler and department manager

Status Change in Interactions (Updated)

  • Staff: only pending, processing, resolved (no "closed" option)
  • Manager/Admin: all options including "closed"
  • When "closed" selected: calls ClosingService which validates evidence

File Management (SOP compliant)

  • Files stored on public disk (storage/app/public/feedback-attachments/)
  • Collection system: proof_of_resolution, customer_evidence, general
  • FileService validates: jpg, png, pdf, mp4, mov, max 20MB
  • Temporary signed URLs for private file access
  • FileService::hasCollection() checks existence of required evidence

Dashboard Widgets

  • ResolvedTicketsWidget: table of resolved tickets awaiting closure (manager: filtered to own department)
  • AvgProcessingTimeWidget: 4 stats (resolved count, avg time, pending count, escalated count)
  • HandlerPerformanceWidget: bar chart of avg processing time per handler

Internationalization (i18n)

Configuration

  • Default locale: vi (Vietnamese), fallback: en (English)
  • Set in .env: APP_LOCALE=vi, APP_FALLBACK_LOCALE=en
  • Filament uses app()->getLocale() — no extra panel config needed

Translation Files

lang/
├── en/
│   ├── app.php          # General UI: navigation, labels, status, widgets, blade strings
│   ├── feedback.php     # Feedback domain: services, notifications, interactions
│   └── enums.php        # Enum labels (HandoverStatus, ServiceType, ContractStatus, ContractType)
└── vi/
    ├── app.php
    ├── feedback.php
    └── enums.php

Translation Patterns

// Resource labels — MUST override getPluralLabel() (not auto-translated)
public static function getPluralLabel(): ?string
{
    return __('app.resource_feedbacks');
}

// Navigation group — MUST override getNavigationGroup() (not auto-translated)
public static function getNavigationGroup(): ?string
{
    return __('app.nav_customer_care');
}

// Form fields — MUST add explicit ->label()
TextInput::make('name')->label(__('app.name'))

// Status options
'options' => [
    'pending' => __('app.status_pending'),
    'processing' => __('app.status_processing'),
]

// Enum labels
public function label(): string
{
    return match ($this) {
        self::ACTIVE => __('enums.contract_status.active'),
    };
}

// Widget heading/description — override getHeading()/getDescription()
public function getHeading(): string | Htmlable | null
{
    return __('app.widget_avg_time_by_handler');
}

Key Gotchas

  • $pluralLabel property does NOT auto-translate — must override getPluralLabel()
  • getNavigationGroup() does NOT auto-translate — must call __() explicitly
  • $heading/$description in ChartWidget are raw properties — must override getHeading()/getDescription()
  • Form fields without ->label() use Filament's filament-panels:: namespace (may not resolve correctly)
  • Always add ->label(__('key')) to TextInput, Select, Textarea, etc.

Navigation Structure

Admin Panel (/admin)

Dashboard (AccountWidget + 3 custom widgets)
├── Customer Care / Chăm sóc khách hàng (group)
│   └── Feedbacks / Phản ánh (chat-bubble-left-right, sort=1)
└── Management / Quản lý (group)
    ├── Products / Sản phẩm (building-storefront, sort=1)
    ├── Customers / Khách hàng (user-group, sort=2)
    ├── Channels / Kênh phản ánh (inbox-stack, sort=3)
    ├── Contracts / Hợp đồng (document-text, sort=4)
    ├── Tags / Thẻ (tag, sort=default)
    ├── Activity Logs (clock, sort=7) — admin only
    ├── Users / Người dùng (users, sort=5) — admin only
    └── Roles / Vai trò (shield-check, sort=6) — admin only

Support Panel (/support)

Dashboard (3 custom widgets)
├── Customer Care / Chăm sóc khách hàng (group)
│   └── Feedbacks
└── Management / Quản lý (group)
    ├── Products
    ├── Customers
    ├── Channels
    ├── Tags
    └── Activity Logs

Dashboard widgets: ResolvedTicketsWidget, AvgProcessingTimeWidget, HandlerPerformanceWidget (admin/manager only)

RBAC (Role-Based Access Control) - Spatie

Roles: admin, manager, staff

Granular Permissions (Spatie Permission)

Feedback: view-feedback, create-feedback, update-feedback, delete-feedback, add-interaction, close-ticket, transfer-department

Products: view-product, create-product, update-product, delete-product

Customers: view-customer, create-customer, update-customer, delete-customer

Contracts: view-contract, create-contract, update-contract, delete-contract

Channels: view-channel, create-channel, update-channel, delete-channel

Tags: view-tag, create-tag, update-tag, delete-tag

Dashboard: view-dashboard-widgets, export-data

Permission Matrix

Permission admin manager staff
view-feedback
create-feedback
update-feedback
delete-feedback
add-interaction
close-ticket
transfer-department
view-product
create/update-product
delete-product
view-customer
create/update-customer
delete-customer
view-contract
create/update-contract
delete-contract
view-channel/tag
create/update-channel/tag
delete-channel/tag
view-dashboard-widgets
export-data

Permission check patterns

// In Policies
$user->hasPermissionTo('close-ticket');
$user->hasPermissionTo('view-feedback');

// In Filament
auth()->user()->hasPermissionTo('transfer-department');
auth()->user()->hasPermissionTo('view-dashboard-widgets');

// Role check (still used for admin-only guards)
$user->hasRole('admin');

Filament Key Patterns (v5.6)

Form components use Filament\Forms\Components

use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;

Schema/layout components use Filament\Schemas\Components

use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;

Services access pattern

use Illuminate\Support\Facades\App;
$service = App::make(FileService::class);

// ActivityLogger (static)
\App\Services\ActivityLogger::log('transfer', '...', $feedback, ['dept' => 'CSKH']);

Spatie role check

$user->hasRole('admin');
$user->hasRole(['admin', 'manager']);
$user->assignRole('manager');

Known Gotchas

  1. Navigation group icons: Filament 5 raises exception if both a navigation group AND its child items have icons.
  2. Form component namespaces: Form input components under Filament\Forms\Components\*, layout under Filament\Schemas\Components\*.
  3. Dynamic selects: Use $get function injection, NOT $component->getLivewire()->data[...].
  4. Table name: Model Feedback maps to table feedback (singular).
  5. FileUpload with dehydrated(false): Attachments stored by Filament first; create FeedbackAttachment record directly in after()/afterSave()/afterCreate() — do NOT re-upload via FileService.
  6. Spatie roles in tests: Create roles explicitly in each test (RefreshDatabase rolls back).
  7. Private disk: Files use local disk (storage/app/private/); access via signed URLs from FileService.
  8. ChartWidget properties: $heading and $columnSpan are non-static in Filament's ChartWidget.
  9. Manager closing permission: Manager must be department head (Department.manager_id) to close tickets in that department.
  10. Docker: Entrypoint auto-generates APP_KEY and runs migrations. SQLite persists via volume mount.
  11. i18n - pluralLabel: $pluralLabel property does NOT auto-translate — must override getPluralLabel() and call __().
  12. i18n - navigationGroup: getNavigationGroup() does NOT auto-translate — must return __('key') explicitly.
  13. i18n - ChartWidget heading: $heading/$description are raw properties — must override getHeading()/getDescription().
  14. i18n - form labels: Form fields without ->label() use Filament's filament-panels:: namespace — always add explicit ->label(__('key')).
  15. i18n - Filament vendor: Filament ships 57 Vietnamese translation files — auto-activated when APP_LOCALE=vi.
  16. FileUpload disk: Always use disk('public') for all FileUpload components. NEVER use disk('local') — causes file not found errors.
  17. FileUpload dehydrated: Do NOT use dehydrated(false) — prevents file paths from being available in afterSave()/afterCreate(). Use property to store paths in mutateFormDataBeforeSave().
  18. File metadata safety: Always check $disk->exists($path) before calling $disk->mimeType() or $disk->size() — prevents UnableToRetrieveMetadata error.
  19. FileUpload in Actions: When handling file uploads in Action callbacks (e.g., transfer attachments), file paths are strings (not UploadedFile). Create records directly instead of using FileService::upload().
  20. Tailwind CSS in Filament: Custom blade views render inside Filament layout which does NOT include full Tailwind CSS build. Must add needed utilities to public/css/filament-custom.css with !important.
  21. SVG icon sizing: Use inline width/height attributes on SVG icons (e.g., <svg width="14" height="14">) instead of Tailwind classes (w-3.5 h-3.5) — prevents Filament CSS from overriding sizes.
  22. Dual Panel: Support panel at /support is separate from Admin panel at /admin. Both share the same models/services/policies/database.

File Upload Architecture

┌─────────────────────────────────────────────────────────────────┐
│  FileUpload Component (disk='public')                           │
│  → Filament uploads via AJAX to storage/app/public/             │
│  → File path stored in Livewire state                           │
│                                                                 │
│  Form Submit                                                    │
│  → mutateFormDataBeforeSave() stores paths in property          │
│  → Removes 'attachments' from $data before saving model        │
│                                                                 │
│  afterSave()/afterCreate()                                      │
│  → Uses stored paths to create FeedbackAttachment records       │
│  → Checks $disk->exists($path) before getting metadata         │
│  → Uses asset('storage/' . $path) for URLs                     │
└─────────────────────────────────────────────────────────────────┘

FileUpload Pattern (correct):

// In Form
FileUpload::make('attachments')
    ->disk('public')                    // ALWAYS public
    ->directory('feedback-attachments')
    // Do NOT use ->dehydrated(false)

// In Page class
protected array $pendingAttachments = [];

protected function mutateFormDataBeforeSave(array $data): array
{
    $this->pendingAttachments = $data['attachments'] ?? [];
    unset($data['attachments']);  // Remove before saving model
    return $data;
}

protected function afterSave(): void
{
    $disk = Storage::disk('public');
    foreach ($this->pendingAttachments as $path) {
        if (! $disk->exists($path)) continue;  // Safety check
        
        $this->getRecord()->attachments()->create([
            'path' => $path,
            'disk' => 'public',
            'mime_type' => $disk->mimeType($path),
            'size' => $disk->size($path),
            // ...
        ]);
    }
}

Tailwind CSS in Filament (correct):

// Filament uses its own CSS build, NOT the project's Tailwind build
// Custom blade views need utilities added to public/css/filament-custom.css

// In filament-custom.css:
.flex { display: flex !important; }
.items-center { align-items: center !important; }
.gap-2 { gap: 0.5rem !important; }
// ... add all needed utilities with !important

// For SVG icons, use inline attributes:
<svg width="14" height="14" class="text-blue-600">...</svg>
// NOT: <svg class="w-3.5 h-3.5 text-blue-600">...</svg>