- PermissionSeeder: 29 permissions with role mapping (admin/manager/staff) - Policies updated to use hasPermissionTo() instead of hasRole() - ExportBackup command: JSON per-table backup with chunk processing - UserResource: CRUD users with role assignment (admin only) - RoleResource: CRUD roles with permission assignment (admin only) - UserPolicy + RolePolicy: admin-only access control - ImportCskh: detailed skip logging with color in dry-run mode - Widgets: permission-based visibility checks - Tests: 8/8 pass (21 assertions)
14 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àmPROGRESS.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 | 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/
│ │ └── Widgets/
│ │ ├── ResolvedTicketsWidget.php # Dashboard: resolved tickets table
│ │ ├── AvgProcessingTimeWidget.php # Dashboard: stats overview
│ │ └── HandlerPerformanceWidget.php # Dashboard: bar chart per handler
│ ├── 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
├── 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
localdisk (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
Navigation Structure
Dashboard
├── Customer Care (group)
│ └── Feedbacks (chat-bubble-left-right, sort=1)
└── Management (group)
├── Products (building-storefront, sort=1)
├── Customers (user-group, sort=2)
├── Channels (inbox-stack, sort=3)
├── Tags (tag, sort=default)
├── Users (users, sort=5) — admin only
└── Roles (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
- 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.