components([ Select::make('type') ->options([ 'note' => 'Note', 'status_change' => 'Status Change', 'assignment' => 'Assignment / Forward', ]) ->default('note') ->required() ->live(), TextInput::make('title') ->maxLength(255) ->visible(fn ($get): bool => $get('type') === 'note'), RichEditor::make('content') ->required() ->columnSpanFull(), Select::make('new_status') ->label('New Status') ->options(fn (): array => auth()->user()->hasRole(['admin', 'manager']) ? ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved', 'closed' => 'Closed'] : ['pending' => 'Pending', 'processing' => 'Processing', 'resolved' => 'Resolved'], ) ->visible(fn ($get): bool => $get('type') === 'status_change'), Select::make('new_assignee') ->label('Assign To') ->options(User::pluck('name', 'id')->toArray()) ->searchable() ->visible(fn ($get): bool => $get('type') === 'assignment'), Select::make('department_id') ->label('Target Department') ->options(Department::pluck('name', 'id')->toArray()) ->searchable() ->nullable() ->visible(fn ($get): bool => $get('type') === 'assignment'), FileUpload::make('attachments') ->label('Attachments') ->multiple() ->disk('local') ->directory('feedback-attachments') ->columnSpanFull(), ]); } public function table(Table $table): Table { return $table ->recordTitleAttribute('content') ->columns([ TextColumn::make('created_at') ->dateTime('d/m/Y H:i') ->sortable(), TextColumn::make('user.name') ->label('By'), TextColumn::make('type') ->badge() ->formatStateUsing(fn (string $state): string => match ($state) { 'note' => 'Note', 'status_change' => 'Status Change', 'assignment' => 'Assignment', default => $state, }) ->color(fn (string $state): string => match ($state) { 'note' => 'gray', 'status_change' => 'info', 'assignment' => 'warning', default => 'gray', }), TextColumn::make('content') ->html() ->limit(80) ->wrap(), TextColumn::make('changes') ->label('Details') ->formatStateUsing(function (?array $state): string { if (! $state) return ''; $lines = []; foreach ($state as $field => $values) { if (is_array($values)) { $lines[] = "{$field}: {$values['old']} → {$values['new']}"; } } return implode('
', $lines); }) ->html(), TextColumn::make('attachments_count') ->label('Files') ->counts('attachments') ->badge() ->color(fn (int $state): string => $state > 0 ? 'primary' : 'gray'), ]) ->defaultSort('created_at', 'desc') ->recordActions([ Action::make('viewAttachments') ->label('View Attachments') ->icon('heroicon-o-paper-clip') ->modalHeading('Attachments') ->modalContent(function (FeedbackInteraction $record): \Illuminate\Contracts\View\View { $fileService = App::make(FileService::class); $attachments = $record->attachments; $links = $attachments->map(function ($a) use ($fileService) { return [ 'name' => $a->name, 'url' => $fileService->getTemporaryUrl($a, 60), 'size' => $a->size ? round($a->size / 1024, 1) . ' KB' : 'N/A', 'collection' => $a->collection, 'mime_type' => $a->mime_type, ]; }); return view('filament.resources.feedback.attachment-links', [ 'links' => $links, ]); }) ->visible(fn (FeedbackInteraction $record): bool => $record->attachments()->count() > 0), ]) ->headerActions([ CreateAction::make() ->label('Add Interaction') ->icon('heroicon-o-plus') ->mutateFormDataUsing(function (array $data, $livewire): array { $data['user_id'] = auth()->id(); $data['feedback_id'] = $livewire->getOwnerRecord()->id; if ($data['type'] === 'status_change' && isset($data['new_status'])) { $feedback = $livewire->getOwnerRecord(); if ($data['new_status'] === 'closed') { $this->handleClosingViaService($feedback, auth()->user(), $data); } $data['changes'] = [ 'status' => ['old' => $feedback->status, 'new' => $data['new_status']], ]; $feedback->update(['status' => $data['new_status']]); unset($data['new_status']); } if ($data['type'] === 'assignment' && isset($data['new_assignee'])) { $feedback = $livewire->getOwnerRecord(); $oldAssignee = $feedback->assignedTo?->name ?? 'Unassigned'; $newAssignee = User::find($data['new_assignee'])?->name ?? 'Unknown'; $data['changes'] = [ 'assignment' => ['old' => $oldAssignee, 'new' => $newAssignee], ]; $update = ['assigned_to' => $data['new_assignee']]; if (! empty($data['department_id'])) { $update['current_department_id'] = $data['department_id']; $dept = Department::find($data['department_id']); $data['changes']['department'] = [ 'old' => $feedback->currentDepartment?->name ?? 'N/A', 'new' => $dept?->name ?? 'N/A', ]; } $feedback->update($update); unset($data['new_assignee'], $data['department_id']); } $data['_attachments'] = $data['attachments'] ?? []; unset($data['attachments'], $data['title']); return $data; }) ->after(function (FeedbackInteraction $record, array $data): void { $attachments = $data['_attachments'] ?? []; if (! empty($attachments)) { $disk = Storage::disk('local'); foreach ($attachments as $path) { $record->attachments()->create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'feedback_id' => $record->feedback_id, 'user_id' => auth()->id(), 'name' => basename($path), 'path' => $path, 'disk' => 'local', 'collection' => 'general', 'mime_type' => $disk->mimeType($path), 'size' => $disk->size($path), ]); } } if ($record->type === 'assignment') { Notification::make() ->title('Feedback assigned successfully') ->success() ->send(); } if ($record->type === 'status_change') { Notification::make() ->title('Status updated successfully') ->success() ->send(); } }), ]); } protected function handleClosingViaService($feedback, $actor, array &$data): void { try { $closingService = App::make(ClosingService::class); $closingService->close($feedback, $actor); } catch (HttpException $e) { Notification::make() ->title($e->getMessage()) ->danger() ->send(); unset($data['new_status']); throw new \Filament\Exceptions\SilentActionException; } catch (ValidationException $e) { Notification::make() ->title(collect($e->errors())->flatten()->first()) ->danger() ->send(); unset($data['new_status']); throw new \Filament\Exceptions\SilentActionException; } } }