components([ Select::make('type') ->options([ 'note' => __('feedback.interaction_type_note'), 'status_change' => __('feedback.interaction_type_status_change'), 'assignment' => __('feedback.interaction_type_assignment'), ]) ->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(__('app.new_status')) ->options(fn (): array => auth()->user()->hasPermissionTo('close-ticket') ? ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved'), 'closed' => __('app.status_closed')] : ['pending' => __('app.status_pending'), 'processing' => __('app.status_processing'), 'resolved' => __('app.status_resolved')], ) ->visible(fn ($get): bool => $get('type') === 'status_change'), Select::make('new_assignee') ->label(__('app.assign_to')) ->options(User::pluck('name', 'id')->toArray()) ->searchable() ->visible(fn ($get): bool => $get('type') === 'assignment'), Select::make('department_id') ->label(__('app.target_department')) ->options(Department::pluck('name', 'id')->toArray()) ->searchable() ->nullable() ->visible(fn ($get): bool => $get('type') === 'assignment'), FileUpload::make('attachments') ->label(__('app.attachments')) ->multiple() ->disk('public') ->directory('feedback-attachments') ->acceptedFileTypes(['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'video/quicktime']) ->maxSize(51200) // 50MB in KB ->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(__('app.by')), TextColumn::make('type') ->badge() ->formatStateUsing(fn (string $state): string => match ($state) { 'note' => __('feedback.interaction_type_note'), 'status_change' => __('feedback.interaction_type_status_change'), 'assignment' => __('feedback.interaction_type_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(__('app.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(__('app.files')) ->counts('attachments') ->badge() ->color(fn (int $state): string => $state > 0 ? 'primary' : 'gray'), ]) ->defaultSort('created_at', 'desc') ->recordActions([ Action::make('viewAttachments') ->label(__('feedback.view_attachments')) ->icon('heroicon-o-paper-clip') ->modalHeading(__('app.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(__('feedback.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 ?? __('app.unassigned'); $newAssignee = User::find($data['new_assignee'])?->name ?? __('app.widget_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('public'); foreach ($attachments as $path) { if (! $disk->exists($path)) { continue; } $record->attachments()->create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'feedback_id' => $record->feedback_id, 'user_id' => auth()->id(), 'name' => basename($path), 'path' => $path, 'disk' => 'public', '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(__('feedback.status_updated')) ->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; } } }