'Produk', 'delivery' => 'Pengiriman', 'service' => 'Layanan', 'other' => 'Lainnya', ]; public const PRIORITIES = [ 'low' => 'Rendah', 'medium' => 'Sedang', 'high' => 'Tinggi', ]; public function __construct( InsForgeClient $client, private readonly CustomerService $customers, private readonly ActivityLogger $activity, ) { parent::__construct($client); } protected function table(): string { return 'complaints'; } protected function select(): string { return '*,customers(code,name),orders(order_number)'; } protected function searchableColumns(): array { return ['complaint_number', 'description']; } protected function sortableColumns(): array { return ['complaint_number', 'complaint_date', 'status', 'priority', 'created_at']; } public function createComplaint(array $attributes, ?User $actor = null): array { [$client, $asUser] = $this->scoped(); $number = $this->nextComplaintNumber(); $row = array_filter([ 'complaint_number' => $number, 'customer_id' => $attributes['customer_id'] ?? null, 'order_id' => $attributes['order_id'] ?? null, 'complaint_date' => $attributes['complaint_date'] ?? now()->toDateString(), 'category' => $attributes['category'] ?? 'other', 'description' => $attributes['description'] ?? null, 'priority' => $attributes['priority'] ?? 'medium', 'status' => self::STATUS_OPEN, 'created_by' => $actor?->id, ], fn ($value) => $value !== null && $value !== ''); if (empty($row['customer_id']) || empty($row['description'])) { throw ValidationException::withMessages(['complaint' => 'Pelanggan dan deskripsi wajib diisi.']); } $client->insertRecord($this->table(), [$row], asUser: false); $stored = $client->listRecords($this->table(), [ 'select' => 'id', 'complaint_number' => 'eq.'.$number, 'limit' => 1, ], asUser: $asUser)[0] ?? null; $this->activity->log('complaint_created', 'complaints', $stored['id'] ?? null, "Komplain {$number} dicatat.", $actor); return ['id' => $stored['id'] ?? null, 'complaint_number' => $number]; } public function resolve(string $complaintId, string $resolution, ?User $actor = null): void { [$client, $asUser] = $this->scoped(); $complaint = $client->getRecord($this->table(), $complaintId, asUser: $asUser) ?? throw ValidationException::withMessages(['complaint' => 'Komplain tidak ditemukan.']); if (($complaint['status'] ?? '') !== self::STATUS_OPEN) { throw ValidationException::withMessages(['complaint' => 'Komplain ini sudah ditutup.']); } $isOwner = $actor !== null && $actor->id === ($complaint['created_by'] ?? null); if (! ($actor?->isAdmin() ?? false) && ! $isOwner) { throw new AuthorizationException('Operator hanya dapat menutup komplain miliknya sendiri.'); } $client->updateRecords($this->table(), ['id' => $complaintId], [ 'status' => self::STATUS_RESOLVED, 'resolution' => $resolution, 'resolved_at' => now()->toISOString(), ], asUser: false); $this->activity->log( 'complaint_resolved', 'complaints', $complaintId, "Komplain {$complaint['complaint_number']} ditutup.", $actor, ); } public function canResolve(array $record, ?User $actor): bool { if (($record['status'] ?? '') !== self::STATUS_OPEN) { return false; } return $actor !== null && ($actor->isAdmin() || $actor->id === ($record['created_by'] ?? null)); } /** @return array id => ORD-... */ public function orderOptions(): array { [$client] = $this->scoped(); return collect($client->listRecords('orders', [ 'select' => 'id,order_number', 'deleted_at' => 'eq.null', 'order' => 'created_at.desc', 'limit' => 200, ]))->mapWithKeys(fn (array $row): array => [$row['id'] => $row['order_number']])->all(); } private function nextComplaintNumber(): string { [$client, $asUser] = $this->scoped(); $prefix = 'CMP-'.now()->format('Ymd').'-'; $last = $client->listRecords($this->table(), [ 'select' => 'complaint_number', 'complaint_number' => 'like.'.$prefix.'*', 'order' => 'complaint_number.desc', 'limit' => 1, ], asUser: $asUser)[0]['complaint_number'] ?? null; preg_match('/(\d+)$/', (string) $last, $matches); return $prefix.sprintf('%04d', ((int) ($matches[1] ?? 0)) + 1); } }