Files
minicrm/AGENTS.md
phuongtc 8c6b71cb8a
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: add i18n support for Vietnamese and English
2026-05-04 10:40:39 +00:00

17 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
  • 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.

How to Run

cd /home/phuongtc/vibecode/minicrm
php artisan migrate:fresh --seed
php artisan serve
# Open http://localhost:8000/admin

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

Directory Structure

minicrm/
├── app/
│   ├── Filament/
│   │   ├── Resources/
│   │   │   ├── Feedback/
│   │   │   │   ├── FeedbackResource.php
│   │   │   │   ├── Schemas/FeedbackForm.php
│   │   │   │   ├── Tables/FeedbackTable.php
│   │   │   │   ├── Pages/{Create,Edit,List}Feedback.php
│   │   │   │   └── RelationManagers/InteractionsRelationManager.php
│   │   │   ├── Products/
│   │   │   ├── Customers/
│   │   │   │   └── RelationManagers/FeedbacksRelationManager.php
│   │   │   ├── FeedbackChannels/
│   │   │   ├── FeedbackTags/
│   │   │   ├── Contracts/
│   │   │   ├── Users/
│   │   │   └── Roles/
│   │   └── Widgets/
│   │       ├── ResolvedTicketsWidget.php       # Dashboard: resolved tickets table
│   │       ├── AvgProcessingTimeWidget.php     # Dashboard: stats overview
│   │       └── HandlerPerformanceWidget.php    # Dashboard: bar chart per handler
│   ├── Enums/
│   │   ├── ContractType.php
│   │   ├── ContractStatus.php
│   │   ├── HandoverStatus.php
│   │   └── ServiceType.php
│   ├── Models/
│   │   ├── User.php                     # HasRoles (Spatie), isAdmin(), isManager()
│   │   ├── Department.php               # NEW: name, manager_id → User
│   │   ├── Product.php
│   │   ├── Customer.php
│   │   ├── CustomerProduct.php
│   │   ├── Feedback.php                 # added: current_department_id, is_escalated, transferLogs
│   │   ├── FeedbackChannel.php
│   │   ├── FeedbackTag.php
│   │   ├── FeedbackInteraction.php
│   │   ├── FeedbackAttachment.php       # added: uuid, disk, collection
│   │   └── TicketTransferLog.php        # NEW: department-to-department transfer history
│   ├── Policies/
│   │   ├── FeedbackPolicy.php           # Spatie hasRole() checks
│   │   ├── ProductPolicy.php
│   │   ├── CustomerPolicy.php
│   │   └── FeedbackChannelPolicy.php
│   ├── Services/
│   │   ├── FileService.php              # Upload, validation, temp URLs, collections
│   │   ├── TransferService.php          # Cross-dept transfer + notification
│   │   └── ClosingService.php           # Role-gated closing + evidence check
│   ├── Rules/
│   │   └── RequireProofOfResolution.php # Custom validation: proof_of_resolution required for close
│   ├── Notifications/
│   │   ├── TicketTransferred.php        # DB + Mail for department transfer
│   │   └── TicketClosed.php             # DB + Mail for ticket closure
│   └── Providers/Filament/
│       └── AdminPanelProvider.php
├── lang/                                # i18n translation files
│   ├── en/
│   │   ├── app.php                      # General UI labels, navigation, status, widgets
│   │   ├── feedback.php                 # Feedback domain: services, notifications, interactions
│   │   └── enums.php                    # Enum labels
│   └── vi/
│       ├── app.php
│       ├── feedback.php
│       └── enums.php
├── resources/views/filament/resources/feedback/
│   ├── similar-cases.blade.php
│   └── attachment-links.blade.php          # Modal listing attachments with signed download URLs
├── database/
│   ├── migrations/
│   └── seeders/AfterSalesSeeder.php     # includes departments, logs, attachments
├── tests/
│   ├── Feature/
│   │   ├── ClosingPermissionTest.php    # 4 tests: staff forbid, no-evidence, with-evidence, cross-dept
│   │   └── TransferFlowTest.php         # 2 tests: successful + missing reason
│   └── Pest.php
├── storage/app/private/feedback-attachments/  # Private disk upload directory
├── composer.json
├── PROGRESS.md
└── AGENTS.md

Database Schema

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 (NEW)

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
feedback_channel_id fk -> feedback_channels
assigned_to fk -> users nullable Current handler
current_department_id fk -> departments nullable (NEW) Current department
is_escalated boolean default false (NEW)
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 (NEW)
feedback_id fk -> feedback
feedback_interaction_id fk -> feedback_interactions nullable
user_id fk -> users
name string
path string
disk string default 'local' (NEW)
collection string default 'general' (NEW) 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 (Spatie permission tables: roles, permissions, model_has_roles, etc.)

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

Transfer Department (NEW - SOP compliant)

  • Action on EditFeedback page: 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 (NEW - SOP compliant)

  • Action on EditFeedback page: 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 (Updated - SOP compliant)

  • Files stored on private local disk (storage/app/private/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 (NEW)

  • 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

Dashboard
├── 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)
    ├── Tags / Thẻ (tag, sort=default)
    ├── Users / Người dùng (users, sort=5) — admin only
    └── Roles / Vai trò (shield-check, sort=6) — admin only

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);

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.