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)
This commit is contained in:
282
AGENTS.md
Normal file
282
AGENTS.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
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`
|
||||
```php
|
||||
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`
|
||||
```php
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
```
|
||||
|
||||
### Services access pattern
|
||||
```php
|
||||
use Illuminate\Support\Facades\App;
|
||||
$service = App::make(FileService::class);
|
||||
```
|
||||
|
||||
### Spatie role check
|
||||
```php
|
||||
$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.
|
||||
Reference in New Issue
Block a user