24 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àmdocs/I18N_GUIDE.md— Hướng dẫn đa ngôn ngữdocs/ENUM_GUIDE.md— Hướng dẫn thêm Enum mớiluutru/— 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 | 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/
│ │ ├── ContractType.php
│ │ ├── ContractStatus.php
│ │ ├── HandoverStatus.php
│ │ └── ServiceType.php
│ ├── Http/
│ │ ├── Controllers/Controller.php
│ │ └── Middleware/HandleUploadErrors.php
│ ├── Models/
│ │ ├── ActivityLog.php # NEW: Audit trail model
│ │ ├── Contract.php
│ │ ├── Customer.php
│ │ ├── CustomerProduct.php
│ │ ├── Department.php
│ │ ├── Feedback.php
│ │ ├── FeedbackAttachment.php
│ │ ├── FeedbackChannel.php
│ │ ├── FeedbackInteraction.php
│ │ ├── FeedbackTag.php
│ │ ├── Handover.php
│ │ ├── Product.php
│ │ ├── ProductService.php
│ │ ├── TicketTransferLog.php
│ │ └── User.php # HasRoles (Spatie), isAdmin(), isManager()
│ ├── 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 |
departments
| 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 |
| customer_product_id | fk -> customer_product nullable | null = general feedback |
| contract_id | fk -> contracts nullable | (NEW) Snapshot contract |
| 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, customer_product, 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-logshoặ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
publicdisk (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
$pluralLabelproperty does NOT auto-translate — must overridegetPluralLabel()getNavigationGroup()does NOT auto-translate — must call__()explicitly$heading/$descriptionin ChartWidget are raw properties — must overridegetHeading()/getDescription()- Form fields without
->label()use Filament'sfilament-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
- Navigation group icons: Filament 5 raises exception if both a navigation group AND its child items have icons.
- Form component namespaces: Form input components under
Filament\Forms\Components\*, layout underFilament\Schemas\Components\*. - Dynamic selects: Use
$getfunction injection, NOT$component->getLivewire()->data[...]. - Table name: Model
Feedbackmaps to tablefeedback(singular). - FileUpload with dehydrated(false): Attachments stored by Filament first; create FeedbackAttachment record directly in after()/afterSave()/afterCreate() — do NOT re-upload via FileService.
- Spatie roles in tests: Create roles explicitly in each test (RefreshDatabase rolls back).
- Private disk: Files use
localdisk (storage/app/private/); access via signed URLs from FileService. - ChartWidget properties:
$headingand$columnSpanare non-static in Filament's ChartWidget. - Manager closing permission: Manager must be department head (
Department.manager_id) to close tickets in that department. - Docker: Entrypoint auto-generates APP_KEY and runs migrations. SQLite persists via volume mount.
- i18n - pluralLabel:
$pluralLabelproperty does NOT auto-translate — must overridegetPluralLabel()and call__(). - i18n - navigationGroup:
getNavigationGroup()does NOT auto-translate — must return__('key')explicitly. - i18n - ChartWidget heading:
$heading/$descriptionare raw properties — must overridegetHeading()/getDescription(). - i18n - form labels: Form fields without
->label()use Filament'sfilament-panels::namespace — always add explicit->label(__('key')). - i18n - Filament vendor: Filament ships 57 Vietnamese translation files — auto-activated when
APP_LOCALE=vi. - FileUpload disk: Always use
disk('public')for all FileUpload components. NEVER usedisk('local')— causes file not found errors. - FileUpload dehydrated: Do NOT use
dehydrated(false)— prevents file paths from being available in afterSave()/afterCreate(). Use property to store paths in mutateFormDataBeforeSave(). - File metadata safety: Always check
$disk->exists($path)before calling$disk->mimeType()or$disk->size()— prevents UnableToRetrieveMetadata error. - 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().
- 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.csswith!important. - SVG icon sizing: Use inline
width/heightattributes 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. - Dual Panel: Support panel at
/supportis 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>