Files
minicrm/app/Filament/Resources/Feedback/RelationManagers/InteractionsRelationManager.php
phuongtc 8c6b71cb8a
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: add i18n support for Vietnamese and English
2026-05-04 10:40:39 +00:00

260 lines
12 KiB
PHP

<?php
namespace App\Filament\Resources\Feedback\RelationManagers;
use App\Models\Department;
use App\Models\FeedbackInteraction;
use App\Models\User;
use App\Services\ClosingService;
use App\Services\FileService;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Illuminate\Support\Facades\Storage;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Facades\App;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
class InteractionsRelationManager extends RelationManager
{
protected static string $relationship = 'interactions';
protected static ?string $title = 'feedback.interaction_history';
public function form(Schema $schema): Schema
{
return $schema
->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('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(__('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[] = "<b>{$field}</b>: {$values['old']}{$values['new']}";
}
}
return implode('<br>', $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('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(__('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;
}
}
}