Files
minicrm/AGENTS.md
phuongtc 887765bbd7 feat: AfterSales CRM - SOP compliant real-estate customer care system
- Departments + cross-department transfer workflow
- Role-based closing (staff→resolved, manager→closed) with proof_of_resolution
- Private file storage with collection system + temporary signed URLs
- Dashboard: resolved tickets table, stats, handler performance bar chart
- Spatie RBAC (admin/manager/staff)
- Notifications: TicketTransferred, TicketClosed (database + mail)
- Pest tests: 8 tests, 21 assertions
- Docker production-ready (Dockerfile + compose + entrypoint)
2026-04-27 05:29:48 +00:00

11 KiB

AfterSales CRM - AI Agent Instructions

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

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
├── database/
│   ├── migrations/
│   └── seeders/AfterSalesSeeder.php     # includes departments, logs, attachments
├── tests/
│   ├── Feature/
│   │   ├── ClosingPermissionTest.php    # 3 tests: staff forbid, no-evidence, with-evidence
│   │   └── 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)

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)

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

RBAC (Role-Based Access Control) - Spatie

Roles: admin, manager, staff

Permission admin manager staff
View products/customers/channels/tags
Create/Update products/customers/channels
Delete anything
View feedbacks
Create/Update feedbacks
Delete feedbacks
Add interactions
Close ticket (status→closed) ✗ (403)
Transfer department
Dashboard widgets

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.