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:
2026-04-27 05:29:48 +00:00
parent 8ad7826f56
commit 887765bbd7
134 changed files with 17416 additions and 45 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Services;
use App\Models\Department;
use App\Models\Feedback;
use App\Models\TicketTransferLog;
use App\Models\User;
use Illuminate\Validation\ValidationException;
class TransferService
{
/**
* Transfer a feedback to another department.
*/
public function transfer(
Feedback $feedback,
Department $toDepartment,
string $reason,
User $sender,
?int $newHandlerId = null,
): TicketTransferLog {
if (empty(trim($reason))) {
throw ValidationException::withMessages([
'reason' => __('Vui lòng nhập lý do chuyển tiếp.'),
]);
}
$fromDepartmentId = $feedback->current_department_id;
$log = TicketTransferLog::create([
'feedback_id' => $feedback->id,
'from_department_id' => $fromDepartmentId,
'to_department_id' => $toDepartment->id,
'sender_id' => $sender->id,
'reason' => $reason,
]);
$feedback->update([
'current_department_id' => $toDepartment->id,
'assigned_to' => $newHandlerId ?? $toDepartment->manager_id ?? null,
]);
$this->notifyDepartmentManager($toDepartment, $feedback, $log, $sender);
return $log;
}
protected function notifyDepartmentManager(Department $department, Feedback $feedback, TicketTransferLog $log, User $sender): void
{
$manager = $department->manager;
if ($manager) {
$manager->notify(new \App\Notifications\TicketTransferred($feedback, $log, $sender));
}
}
}