ui updates for ticket layout
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('helpdesk_comments', static function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('helpdesk_comments', 'deleted_by')) {
|
||||
$table->foreignId('deleted_by')->nullable()->after('tombstoned_at')->constrained('users')->nullOnDelete();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('helpdesk_comments', 'delete_reason')) {
|
||||
$table->string('delete_reason', 255)->nullable()->after('deleted_by');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('helpdesk_comments', static function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('helpdesk_comments', 'deleted_by')) {
|
||||
$table->dropConstrainedForeignId('deleted_by');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'delete_reason')) {
|
||||
$table->dropColumn('delete_reason');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class TicketCommentDeleteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Tombstone a ticket response. Only MPBA/platform super administrators may delete responses.
|
||||
*/
|
||||
public function __invoke(Request $request, Ticket $ticket, Comment $comment): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless($user !== null && method_exists($user, 'isMpbaPlatformAdmin') && $user->isMpbaPlatformAdmin(), 403);
|
||||
abort_unless((int) $comment->ticket_id === (int) $ticket->id, 404);
|
||||
|
||||
DB::transaction(function () use ($comment, $ticket, $user): void {
|
||||
$updates = [];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'tombstoned_at')) {
|
||||
$updates['tombstoned_at'] = now();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'deleted_by')) {
|
||||
$updates['deleted_by'] = (int) $user->id;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'delete_reason')) {
|
||||
$updates['delete_reason'] = 'Deleted by MPBA platform administrator.';
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_sequence')) {
|
||||
$updates['sync_sequence'] = ((int) ($comment->sync_sequence ?? 0)) + 1;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_hash')) {
|
||||
$updates['sync_hash'] = hash('sha256', implode('|', [
|
||||
(string) ($comment->guid ?? $comment->id),
|
||||
'deleted',
|
||||
now()->toISOString(),
|
||||
]));
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
if ($updates !== []) {
|
||||
DB::table('helpdesk_comments')
|
||||
->where('id', $comment->id)
|
||||
->update($updates);
|
||||
} else {
|
||||
$comment->delete();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-comment-deleted', 'Response deleted.');
|
||||
}
|
||||
}
|
||||
@@ -303,6 +303,9 @@ final class TicketShow extends Component
|
||||
|
||||
$comments = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->when(Schema::hasColumn('helpdesk_comments', 'tombstoned_at'), static function ($query): void {
|
||||
$query->whereNull('tombstoned_at');
|
||||
})
|
||||
->when(! $this->canSeeInternalNotes(), static function ($query): void {
|
||||
$query->where('is_internal', false);
|
||||
})
|
||||
|
||||
@@ -35,6 +35,8 @@ final class Comment extends Model
|
||||
'replicated_at',
|
||||
'synced_at',
|
||||
'tombstoned_at',
|
||||
'deleted_by',
|
||||
'delete_reason',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
@@ -44,6 +46,7 @@ final class Comment extends Model
|
||||
'replicated_at' => 'datetime',
|
||||
'synced_at' => 'datetime',
|
||||
'tombstoned_at' => 'datetime',
|
||||
'deleted_by' => 'integer',
|
||||
];
|
||||
|
||||
/** @return BelongsTo<Ticket, Comment> */
|
||||
@@ -64,6 +67,12 @@ final class Comment extends Model
|
||||
return $this->hasMany(self::class, 'parent_id')->oldest();
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, Comment> */
|
||||
public function deleter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'deleted_by');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, Comment> */
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
'resources/css/app.css',
|
||||
'resources/js/app.js',
|
||||
])
|
||||
@vite([
|
||||
'Resources/assets/css/app.css',
|
||||
'Resources/assets/js/app.js',
|
||||
], 'build/helpdesk')
|
||||
|
||||
@include('helpdesk::partials.page-header', [
|
||||
'eyebrow' => 'Helpdesk',
|
||||
'title' => 'Create ticket',
|
||||
|
||||
@@ -181,34 +181,31 @@
|
||||
|
||||
.hd-helpdesk-surface [data-helpdesk-file-button] {
|
||||
display: inline-flex !important;
|
||||
width: auto !important;
|
||||
min-width: 144px !important;
|
||||
height: 42px !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
align-self: center !important;
|
||||
gap: 9px !important;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid rgba(34, 197, 94, 0.55) !important;
|
||||
background: linear-gradient(135deg, #16a34a, #22c55e) !important;
|
||||
gap: 10px !important;
|
||||
min-width: 168px !important;
|
||||
height: 48px !important;
|
||||
padding: 0 22px !important;
|
||||
border: 1px solid rgba(134, 239, 172, 0.70) !important;
|
||||
border-radius: 14px !important;
|
||||
background: linear-gradient(135deg, #15803d 0%, #16a34a 55%, #22c55e 100%) !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 10px 22px rgba(22, 163, 74, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.18) !important;
|
||||
font-size: 0.88rem !important;
|
||||
font-weight: 800 !important;
|
||||
box-shadow: 0 14px 28px rgba(22, 163, 74, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.20) !important;
|
||||
font-size: 0.92rem !important;
|
||||
font-weight: 900 !important;
|
||||
line-height: 1 !important;
|
||||
letter-spacing: 0.005em !important;
|
||||
padding: 0 16px !important;
|
||||
text-align: center !important;
|
||||
white-space: nowrap !important;
|
||||
vertical-align: middle !important;
|
||||
transition: border-color 150ms ease, background 150ms ease, color 150ms ease, transform 150ms ease, box-shadow 150ms ease !important;
|
||||
cursor: pointer !important;
|
||||
transition: transform 150ms ease, box-shadow 150ms ease, background 150ms ease !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface [data-helpdesk-file-button]:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
border-color: rgba(134, 239, 172, 0.85) !important;
|
||||
background: linear-gradient(135deg, #15803d, #16a34a) !important;
|
||||
background: linear-gradient(135deg, #166534 0%, #15803d 55%, #16a34a 100%) !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 12px 26px rgba(22, 163, 74, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.20) !important;
|
||||
box-shadow: 0 16px 32px rgba(22, 163, 74, 0.36), inset 0 1px 0 rgba(255, 255, 255, 0.22) !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface [data-helpdesk-file-button] i,
|
||||
@@ -217,15 +214,18 @@
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface [data-helpdesk-file-button] i {
|
||||
height: 18px !important;
|
||||
width: 18px !important;
|
||||
border-radius: 6px !important;
|
||||
background: rgba(255, 255, 255, 0.16) !important;
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
border-radius: 8px !important;
|
||||
background: rgba(255, 255, 255, 0.18) !important;
|
||||
color: #ffffff !important;
|
||||
font-size: 0.74rem !important;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface .hd-select {
|
||||
@@ -505,45 +505,44 @@
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
<details class="mt-5 overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/40">
|
||||
<summary class="group flex cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 transition hover:bg-zinc-100/80 dark:hover:bg-zinc-900/70 [&::-webkit-details-marker]:hidden">
|
||||
<span class="flex min-w-0 items-center gap-3">
|
||||
<span class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/70 dark:bg-blue-950/50 dark:text-blue-300">
|
||||
<details class="mt-5 overflow-hidden rounded-[1.5rem] border border-zinc-200 bg-white/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/50">
|
||||
<summary class="group flex cursor-pointer list-none items-center justify-between gap-4 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-6 py-5 text-white transition hover:from-slate-900 hover:via-blue-900 hover:to-slate-900 [&::-webkit-details-marker]:hidden">
|
||||
<span class="flex min-w-0 items-center gap-4">
|
||||
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-blue-400/30 bg-blue-500/15 text-blue-200 shadow-inner">
|
||||
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-xs font-black uppercase tracking-[0.22em] text-blue-700 dark:text-blue-300">Attachments</span>
|
||||
<span class="block truncate text-sm font-semibold text-zinc-600 dark:text-zinc-400">Pin screenshots, logs and supporting documents.</span>
|
||||
<span class="block text-xs font-black uppercase tracking-[0.28em] text-blue-200">Attachments</span>
|
||||
<span class="block text-xl font-black tracking-tight text-white">Supporting files</span>
|
||||
<span class="block truncate text-sm font-semibold text-zinc-300">Pin screenshots, logs and supporting documents.</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex shrink-0 items-center gap-3">
|
||||
<span class="rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-bold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200" data-helpdesk-file-count>0 files</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs text-zinc-500 transition group-open:rotate-180" aria-hidden="true"></i>
|
||||
<span class="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-black text-white" data-helpdesk-file-count>0 files</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs text-blue-200 transition group-open:rotate-180" aria-hidden="true"></i>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="border-t border-zinc-200 px-5 py-5 text-sm dark:border-zinc-800">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white/70 p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/45">
|
||||
<div data-helpdesk-attachment-picker-row>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/70 dark:bg-blue-950/55 dark:text-blue-300">
|
||||
<i class="fa-solid fa-paperclip text-sm" aria-hidden="true"></i>
|
||||
<div class="border-t border-zinc-200 p-6 dark:border-zinc-800">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/70">
|
||||
<div class="flex min-h-[5.25rem] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex min-w-0 items-center gap-4">
|
||||
<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-zinc-300 bg-white text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200">
|
||||
<i class="fa-solid fa-file-arrow-up text-sm" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-bold text-zinc-900 dark:text-zinc-100">Attach supporting files</span>
|
||||
<span class="block text-sm font-black text-zinc-900 dark:text-zinc-100">Attach supporting evidence</span>
|
||||
<span class="block truncate text-xs font-semibold text-zinc-500 dark:text-zinc-400" data-helpdesk-file-label>No files attached</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label
|
||||
data-helpdesk-file-button
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Attach files</span>
|
||||
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
|
||||
</label>
|
||||
<div class="flex shrink-0 items-center justify-center sm:min-w-[190px]">
|
||||
<label data-helpdesk-file-button>
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Attach files</span>
|
||||
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
|
||||
|
||||
@@ -118,11 +118,22 @@
|
||||
@endif
|
||||
|
||||
@if ($currentTicket !== null)
|
||||
<div class="mt-4 flex justify-end">
|
||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||
<button type="button" dusk="reply-to-comment-{{ $comment->id }}" data-helpdesk-reply-toggle="{{ $replyFormId }}" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
|
||||
<i class="fa-solid fa-reply mr-2" aria-hidden="true"></i>
|
||||
Respond
|
||||
</button>
|
||||
|
||||
@if (auth()->user()?->isMpbaPlatformAdmin())
|
||||
<form method="POST" action="{{ route('helpdesk.tickets.comments.destroy', [$currentTicket, $comment]) }}" onsubmit="return confirm('Delete this response? This will remove it from the visible conversation but keep the replication identity/audit trail.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" dusk="delete-comment-{{ $comment->id }}" class="inline-flex items-center justify-center rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400/70">
|
||||
<i class="fa-solid fa-trash-can mr-2" aria-hidden="true"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form id="{{ $replyFormId }}" method="POST" action="{{ route('helpdesk.tickets.comments.store', $currentTicket) }}" enctype="multipart/form-data" class="mt-4 hidden rounded-xl border border-red-200 bg-red-50/60 p-4 dark:border-red-900 dark:bg-red-950/20" data-helpdesk-comment-form>
|
||||
@@ -151,14 +162,33 @@
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
<div class="mt-4 rounded-xl border border-dashed border-blue-300 p-3 dark:border-blue-800">
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-xs font-bold text-white shadow-sm hover:bg-blue-700">
|
||||
<i class="fa-solid fa-thumbtack" aria-hidden="true"></i>
|
||||
Pin attachments
|
||||
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
|
||||
</label>
|
||||
<span class="ml-2 text-xs font-semibold text-zinc-500" data-helpdesk-file-label>No files pinned</span>
|
||||
</div>
|
||||
<details class="mt-4 overflow-hidden rounded-xl border border-zinc-200 bg-white/80 shadow-sm dark:border-zinc-800 dark:bg-zinc-950/50">
|
||||
<summary class="group flex cursor-pointer list-none items-center justify-between gap-3 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white [&::-webkit-details-marker]:hidden">
|
||||
<span class="flex min-w-0 items-center gap-3">
|
||||
<span class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-blue-400/30 bg-blue-500/15 text-blue-200">
|
||||
<i class="fa-solid fa-paperclip text-xs" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-[10px] font-black uppercase tracking-[0.22em] text-blue-200">Attachments</span>
|
||||
<span class="block truncate text-xs font-semibold text-zinc-300">Pin response files.</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex shrink-0 items-center gap-2">
|
||||
<span class="rounded-full border border-white/10 bg-white/10 px-2.5 py-1 text-[10px] font-black text-white" data-helpdesk-file-count>0 files</span>
|
||||
<i class="fa-solid fa-chevron-down text-[10px] text-blue-200 transition group-open:rotate-180" aria-hidden="true"></i>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="p-4">
|
||||
<div class="flex min-h-[4.5rem] flex-col gap-3 rounded-xl border border-zinc-200 bg-zinc-50/80 p-4 dark:border-zinc-800 dark:bg-zinc-950/70 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span class="text-xs font-semibold text-zinc-500 dark:text-zinc-400" data-helpdesk-file-label>No files attached</span>
|
||||
<label data-helpdesk-file-button>
|
||||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||||
<span>Attach files</span>
|
||||
<input name="attachments[]" type="file" multiple class="sr-only" data-helpdesk-file-input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" data-helpdesk-reply-toggle="{{ $replyFormId }}" class="inline-flex items-center justify-center rounded-lg bg-slate-600 px-3 py-2 text-xs font-bold text-white shadow-sm transition hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400/70">Cancel</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
|
||||
use Modules\Helpdesk\Http\Controllers\HelpdeskController;
|
||||
use Modules\Helpdesk\Http\Controllers\TicketControlsController;
|
||||
use Modules\Helpdesk\Http\Controllers\TicketCommentDeleteController;
|
||||
use Modules\Helpdesk\Http\Controllers\TicketCommentStoreController;
|
||||
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
|
||||
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
||||
@@ -31,6 +32,7 @@ Route::post('/tickets/{ticket}/agent', [TicketControlsController::class, 'update
|
||||
Route::post('/tickets/{ticket}/status', [TicketControlsController::class, 'updateStatus'])->name('tickets.status.update');
|
||||
Route::post('/tickets/{ticket}/priority', [TicketControlsController::class, 'updatePriority'])->name('tickets.priority.update');
|
||||
Route::post('/tickets/{ticket}/comments', TicketCommentStoreController::class)->name('tickets.comments.store');
|
||||
Route::delete('/tickets/{ticket}/comments/{comment}', TicketCommentDeleteController::class)->name('tickets.comments.destroy');
|
||||
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
||||
Route::get('/attachments/{attachment}/download', AttachmentDownloadController::class)
|
||||
->whereNumber('attachment')
|
||||
|
||||
@@ -5,9 +5,13 @@ declare(strict_types=1);
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\TicketShow;
|
||||
use Modules\Helpdesk\Models\Attachment;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
@@ -111,6 +115,54 @@ final class TicketConversationTest extends TestCase
|
||||
self::assertSame('RE: Parent response', $reply->subject);
|
||||
}
|
||||
|
||||
|
||||
public function test_post_endpoint_accepts_multiple_comment_attachments(): void
|
||||
{
|
||||
Storage::fake((string) config('helpdesk.disk', 'public'));
|
||||
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->post(route('helpdesk.tickets.comments.store', $ticket), [
|
||||
'subject' => 'Attachments attached',
|
||||
'commentBody' => '<p>Files attached.</p>',
|
||||
'attachments' => [
|
||||
UploadedFile::fake()->create('first-log.txt', 8, 'text/plain'),
|
||||
UploadedFile::fake()->create('second-log.txt', 8, 'text/plain'),
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('helpdesk.tickets.show', $ticket));
|
||||
|
||||
$comment = Comment::query()->where('ticket_id', $ticket->id)->firstOrFail();
|
||||
|
||||
self::assertSame(2, Attachment::query()->where('comment_id', $comment->id)->count());
|
||||
}
|
||||
|
||||
public function test_mpba_platform_admin_can_delete_comment(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$company = Company::query()->create([
|
||||
'name' => 'MPBA Limited',
|
||||
'slug' => 'mpba-limited',
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
'is_system_company' => true,
|
||||
]);
|
||||
$admin = $this->user(['company_id' => $company->id, 'is_platform_admin' => true]);
|
||||
$owner = $this->user(['company_id' => $company->id]);
|
||||
$ticket = $this->ticket(['owner' => $owner, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
$comment = app(\Modules\Helpdesk\Services\TicketService::class)->comment($ticket, $owner, '<p>Delete me</p>');
|
||||
|
||||
$this->actingAs($admin)
|
||||
->delete(route('helpdesk.tickets.comments.destroy', [$ticket, $comment]))
|
||||
->assertRedirect(route('helpdesk.tickets.show', $ticket));
|
||||
|
||||
$comment->refresh();
|
||||
self::assertNotNull($comment->tombstoned_at);
|
||||
}
|
||||
|
||||
public function test_agent_can_update_status_to_closed_and_priority(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
@@ -333,3 +333,14 @@ versions:
|
||||
date: 2026-06-17
|
||||
changes:
|
||||
- Centred the attachment chooser content vertically and changed the attachment action to a professional green button treatment.
|
||||
|
||||
- version: 1.9.19
|
||||
date: 2026-06-17
|
||||
changes:
|
||||
- Moved response attachment selection into the professional Supporting files card with a centred green attachment action.
|
||||
- Preserved multiple file uploads for comments, responses, and response-to-response replies.
|
||||
- Added MPBA platform administrator response deletion using replication-safe tombstoning rather than destructive removal.
|
||||
- Added deletion audit fields for helpdesk comments.
|
||||
- Removed direct Helpdesk Vite manifest loading from ticket create when the module build manifest is not present.
|
||||
- Updated Helpdesk conversation feature coverage for multiple attachments and super-admin response deletion.
|
||||
- Kept module.json version in sync with changelog.yaml.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.18"
|
||||
"version": "1.9.19"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user