Add file attachment functionality to ticket messages
This commit is contained in:
@@ -9,28 +9,79 @@ use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Rule;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
#[DisabledIf('tickets_disabled')]
|
||||
class Show extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
#[Locked]
|
||||
public Ticket $ticket;
|
||||
|
||||
#[Rule('required', 'string')]
|
||||
public string $message;
|
||||
public string $message = '';
|
||||
|
||||
#[Rule(['attachments.*' => 'image|max:5120'])] // 5MB max per image
|
||||
public array $attachments = [];
|
||||
|
||||
public function updatedAttachments()
|
||||
{
|
||||
$this->validateOnly('attachments.*');
|
||||
}
|
||||
|
||||
public function removeAttachment($index)
|
||||
{
|
||||
unset($this->attachments[$index]);
|
||||
$this->attachments = array_values($this->attachments);
|
||||
}
|
||||
|
||||
public function uploadImage()
|
||||
{
|
||||
$this->validate([
|
||||
'attachments.*' => 'image|max:5120',
|
||||
]);
|
||||
|
||||
$uploadedPaths = [];
|
||||
foreach ($this->attachments as $attachment) {
|
||||
$path = $attachment->store('ticket-attachments', 'public');
|
||||
$uploadedPaths[] = [
|
||||
'path' => $path,
|
||||
'name' => $attachment->getClientOriginalName(),
|
||||
'url' => Storage::url($path),
|
||||
];
|
||||
}
|
||||
|
||||
return $uploadedPaths;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
// Handle attachments
|
||||
$attachmentData = [];
|
||||
if (!empty($this->attachments)) {
|
||||
foreach ($this->attachments as $attachment) {
|
||||
$path = $attachment->store('ticket-attachments', 'public');
|
||||
$attachmentData[] = [
|
||||
'path' => $path,
|
||||
'name' => $attachment->getClientOriginalName(),
|
||||
'url' => Storage::url($path),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->ticket->messages()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'message' => $this->message,
|
||||
'attachments' => !empty($attachmentData) ? $attachmentData : null,
|
||||
]);
|
||||
|
||||
$this->notify('Message sent successfully');
|
||||
|
||||
$this->message = '';
|
||||
$this->attachments = [];
|
||||
$this->dispatch('saved');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ class TicketMessage extends Model
|
||||
'ticket_id',
|
||||
'user_id',
|
||||
'message',
|
||||
'attachments',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attachments' => 'array',
|
||||
];
|
||||
|
||||
public function ticket()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('ticket_messages', function (Blueprint $table) {
|
||||
$table->json('attachments')->nullable()->after('message');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ticket_messages', function (Blueprint $table) {
|
||||
$table->dropColumn('attachments');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -22,4 +22,11 @@ return [
|
||||
'service' => 'Service',
|
||||
'select_service' => 'Select Service',
|
||||
'create' => 'Create',
|
||||
|
||||
// Attachment translations
|
||||
'attachments' => 'Attachments',
|
||||
'click_to_upload' => 'Click to upload',
|
||||
'or_drag_drop' => 'or drag and drop',
|
||||
'image_formats' => 'PNG, JPG, GIF up to 5MB',
|
||||
'uploading' => 'Uploading...',
|
||||
];
|
||||
|
||||
@@ -25,6 +25,26 @@
|
||||
<div class="mt-2 prose dark:prose-invert max-w-none text-color-base">
|
||||
{!! Str::markdown(nl2br(e($message->message)), ['allow_unsafe_links' => false]) !!}
|
||||
</div>
|
||||
|
||||
{{-- Display Attachments --}}
|
||||
@if($message->attachments && count($message->attachments) > 0)
|
||||
<div class="mt-4 pt-4 border-t border-neutral/20">
|
||||
<p class="text-sm font-medium text-neutral-400 mb-2">
|
||||
<x-ri-attachment-2 class="inline size-4 mr-1" />
|
||||
{{ __('ticket.attachments') ?? 'Attachments' }} ({{ count($message->attachments) }})
|
||||
</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
@foreach($message->attachments as $attachment)
|
||||
<a href="{{ $attachment['url'] }}" target="_blank" class="group block">
|
||||
<div class="aspect-square rounded-lg overflow-hidden border border-neutral/30 bg-background/50 hover:border-primary/50 transition-colors">
|
||||
<img src="{{ $attachment['url'] }}" alt="{{ $attachment['name'] }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-neutral-400 truncate group-hover:text-primary transition-colors">{{ $attachment['name'] }}</p>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -37,6 +57,57 @@
|
||||
</div>
|
||||
<x-easymde-editor />
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-color-base mb-2">
|
||||
<x-ri-attachment-2 class="inline size-4 mr-1" />
|
||||
{{ __('ticket.attachments') ?? 'Attachments' }}
|
||||
</label>
|
||||
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<label for="attachments" class="flex flex-col items-center justify-center w-full h-32 border-2 border-neutral border-dashed rounded-lg cursor-pointer bg-background/50 hover:bg-background/80 transition-colors">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<x-ri-upload-cloud-2-line class="size-8 mb-2 text-neutral-400" />
|
||||
<p class="mb-2 text-sm text-neutral-400">
|
||||
<span class="font-semibold">{{ __('ticket.click_to_upload') ?? 'Click to upload' }}</span> {{ __('ticket.or_drag_drop') ?? 'or drag and drop' }}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500">{{ __('ticket.image_formats') ?? 'PNG, JPG, GIF up to 5MB' }}</p>
|
||||
</div>
|
||||
<input id="attachments" type="file" class="hidden" wire:model="attachments" multiple accept="image/*" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div wire:loading wire:target="attachments" class="mt-2">
|
||||
<div class="flex items-center gap-2 text-sm text-primary">
|
||||
<x-ri-loader-4-line class="size-4 animate-spin" />
|
||||
{{ __('ticket.uploading') ?? 'Uploading...' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Uploaded Files -->
|
||||
@if(count($attachments) > 0)
|
||||
<div class="mt-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
@foreach($attachments as $index => $attachment)
|
||||
<div class="relative group">
|
||||
<div class="aspect-square rounded-lg overflow-hidden border border-neutral bg-background">
|
||||
<img src="{{ $attachment->temporaryUrl() }}" alt="Preview" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<button type="button" wire:click="removeAttachment({{ $index }})"
|
||||
class="absolute -top-2 -right-2 bg-error text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-error/80">
|
||||
<x-ri-close-line class="size-4" />
|
||||
</button>
|
||||
<p class="mt-1 text-xs text-neutral-400 truncate">{{ $attachment->getClientOriginalName() }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error('attachments.*')
|
||||
<p class="mt-2 text-sm text-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-button.primary class="py-3 text-base font-bold transition-all transform rounded-lg shadow-lg hover:bg-primary focus:outline-none focus:ring-4 focus:ring-primary-500 hover:-translate-y-0.5">
|
||||
{{ __('ticket.reply') }}
|
||||
|
||||
Reference in New Issue
Block a user