diff --git a/src/Http/Controllers/TicketController.php b/src/Http/Controllers/TicketController.php index c0943d8..ae2e2a1 100644 --- a/src/Http/Controllers/TicketController.php +++ b/src/Http/Controllers/TicketController.php @@ -493,8 +493,33 @@ class TicketController extends Controller } } + // Issue #8: capture the previous assignee BEFORE the write, so a genuine + // hand-off can be told from a save that merely touched other fields. + $previousAssignee = $ticket->assigned_to; + $ticket->update($validated); + $newAssignee = $ticket->assigned_to; + $handedOver = $newAssignee + && $newAssignee !== $previousAssignee + && $newAssignee !== Auth::id(); // assigning to yourself needs no telling + + if ($handedOver) { + $agent = \App\Models\User::find($newAssignee); + $email = trim((string) ($agent?->email ?? '')); + + if ($email !== '') { + \Dashboard\Ticketing\Support\TicketingNotifier::deliver( + \Dashboard\Ticketing\Support\TicketingNotifier::ASSIGNED, + $email, + "[{$ticket->number}] Assigned to you: {$ticket->title}", + '
'.e(Auth::user()?->name ?? 'Somebody').' assigned this ticket to you.
' + .'', + $ticket, + ); + } + } + return back()->with('success', 'Ticket updated.'); } diff --git a/src/Http/Controllers/TicketMessageController.php b/src/Http/Controllers/TicketMessageController.php index faa63f4..bd02632 100644 --- a/src/Http/Controllers/TicketMessageController.php +++ b/src/Http/Controllers/TicketMessageController.php @@ -62,9 +62,34 @@ class TicketMessageController extends Controller return back()->with('success', 'Message sent.'); } + /** + * Issue #8: this was an empty placeholder, so nobody has ever been told their + * ticket was answered. Goes through the Notifications platform so it lands in the + * shared Notification Log and honours the person's preferences, rather than + * reaching for Mail:: directly as the old comment suggested. + * + * Only ever called for a non-internal agent reply — see the caller. + */ private function notifySubmitter(Ticket $ticket, TicketMessage $message): void { - // Placeholder for email notification - // In production: Mail::to($submitterEmail)->send(new TicketReplyMail($ticket, $message)); + $submitter = \App\Models\User::find($ticket->submitter_id); + $email = trim((string) ($submitter?->email ?? '')); + + if ($email === '') { + return; // a ticket raised on behalf of somebody with no address + } + + $url = route('ticketing.show', $ticket->id); + $author = $message->user_id ? (\App\Models\User::find($message->user_id)?->name ?? 'An agent') : 'An agent'; + + \Dashboard\Ticketing\Support\TicketingNotifier::deliver( + \Dashboard\Ticketing\Support\TicketingNotifier::REPLIED, + $email, + "[{$ticket->number}] {$ticket->title}", + ''.e($author).' replied to your ticket:
' + .''.nl2br(e($message->body)).'' + .'', + $ticket, + ); } } diff --git a/src/Support/TicketingNotifier.php b/src/Support/TicketingNotifier.php new file mode 100644 index 0000000..d9793ee --- /dev/null +++ b/src/Support/TicketingNotifier.php @@ -0,0 +1,132 @@ +send(new TicketReplyMail(…))", so + * nobody has ever been told their ticket was answered. + * + * TWO CATEGORIES, matching the two moments a person is waiting on somebody else: + * + * - ticketing.replied → the submitter, when an agent answers. Internal notes are + * excluded by the caller: the whole point of an internal note is that the + * submitter does not see it. + * - ticketing.assigned → the agent a ticket has just been handed to. Skipped when + * somebody assigns a ticket to themselves, which needs no telling. + * + * Neither is mandatory. A reply notice is a convenience, not a security or + * financial record, so a person may turn it off in their notification preferences. + * + * SHIP-DARK SAFETY. available() is false when the platform is absent, and every + * caller treats a false return as "nothing was sent" rather than an error — there is + * no direct-mail fallback to run, because there was never any direct mail here. + * DELIBERATELY NOT a manifest dependency: requires.apps is a HARD requirement (a + * merely-disabled dep fails dashboard:doctor, which gates update.sh and promote.sh), + * and Notifications is a beta app that ships dark. Add it once Notifications is + * stability: stable. + */ +final class TicketingNotifier +{ + public const REPLIED = 'ticketing.replied'; + + public const ASSIGNED = 'ticketing.assigned'; + + /** @var array