added correct SLA support
Awaiting Response No qualifying response yet Due Soon SLA is approaching its deadline MET SLA target achieved on time BREACHED SLA target missed
This commit is contained in:
@@ -320,16 +320,16 @@ final class TicketShow extends Component
|
||||
|
||||
$this->hydrateTicketUsers($ticket);
|
||||
|
||||
$comments = $ticket->comments()
|
||||
$comments = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->active()
|
||||
->when(! $this->canSeeInternalNotes(), static function ($query): void {
|
||||
$query->where('is_internal', false);
|
||||
})
|
||||
->with(['user.company', 'attachments', 'replies.user.company', 'replies.attachments'])
|
||||
->with(['user.company', 'attachments'])
|
||||
->oldest()
|
||||
->get();
|
||||
|
||||
$ticket->setRelation('comments', $comments);
|
||||
$ticket->setAttribute('public_replies_count', $comments->where('is_internal', false)->count());
|
||||
$ticket->setAttribute('time_spent_minutes', (int) $ticket->getRawOriginal('time_spent_minutes'));
|
||||
$ticket->setRelation('comments', $this->buildCommentTree($comments->groupBy('parent_id')));
|
||||
@@ -337,7 +337,9 @@ final class TicketShow extends Component
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format the total logged ticket time for display.
|
||||
*/
|
||||
public function formattedTimeSpent(): string
|
||||
{
|
||||
$minutes = (int) $this->ticket->time_spent_minutes;
|
||||
@@ -355,6 +357,203 @@ final class TicketShow extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the first-response SLA headline for the SLA panel.
|
||||
*/
|
||||
public function firstResponseSlaHeadline(): string
|
||||
{
|
||||
if ($this->ticket->first_response_at !== null) {
|
||||
return $this->firstResponseWasBreached() ? 'Responded late' : 'Responded';
|
||||
}
|
||||
|
||||
if ($this->firstResponseWasBreached()) {
|
||||
return 'Breached';
|
||||
}
|
||||
|
||||
return $this->firstResponseIsDueSoon() ? 'Due soon' : 'Awaiting public reply';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first-response SLA supporting text.
|
||||
*/
|
||||
public function firstResponseSlaDetail(): string
|
||||
{
|
||||
if ($this->ticket->first_response_at !== null) {
|
||||
return 'Responded in '.$this->formatDurationBetween($this->ticket->created_at, $this->ticket->first_response_at);
|
||||
}
|
||||
|
||||
if ($this->ticket->sla_first_response_due_at === null) {
|
||||
return 'No first-response target is set.';
|
||||
}
|
||||
|
||||
return 'Due '.$this->ticket->sla_first_response_due_at->format('d M Y H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first responder display name, if one can be identified.
|
||||
*/
|
||||
public function firstResponderName(): string
|
||||
{
|
||||
$comment = Comment::query()
|
||||
->where('ticket_id', $this->ticket->id)
|
||||
->where('is_internal', false)
|
||||
->whereNull('tombstoned_at')
|
||||
->with('user')
|
||||
->oldest('created_at')
|
||||
->first();
|
||||
|
||||
return $comment?->user?->name ?: 'Unknown responder';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first-response SLA colour classes.
|
||||
*/
|
||||
public function firstResponseSlaClasses(): string
|
||||
{
|
||||
if ($this->firstResponseWasBreached()) {
|
||||
return 'border-red-300 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-100';
|
||||
}
|
||||
|
||||
if ($this->ticket->first_response_at !== null) {
|
||||
return 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-100';
|
||||
}
|
||||
|
||||
if ($this->firstResponseIsDueSoon()) {
|
||||
return 'border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100';
|
||||
}
|
||||
|
||||
return 'border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-100';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolution SLA headline for the SLA panel.
|
||||
*/
|
||||
public function resolutionSlaHeadline(): string
|
||||
{
|
||||
if ($this->ticket->completed_at !== null) {
|
||||
return $this->resolutionWasBreached() ? 'Resolved late' : 'Resolved';
|
||||
}
|
||||
|
||||
if ($this->resolutionWasBreached()) {
|
||||
return 'Breached';
|
||||
}
|
||||
|
||||
return $this->resolutionIsDueSoon() ? 'Due soon' : 'Open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolution SLA supporting text.
|
||||
*/
|
||||
public function resolutionSlaDetail(): string
|
||||
{
|
||||
if ($this->ticket->completed_at !== null) {
|
||||
return 'Resolved in '.$this->formatDurationBetween($this->ticket->created_at, $this->ticket->completed_at);
|
||||
}
|
||||
|
||||
if ($this->ticket->sla_resolution_due_at === null) {
|
||||
return 'No resolution target is set.';
|
||||
}
|
||||
|
||||
return 'Due '.$this->ticket->sla_resolution_due_at->format('d M Y H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolution SLA colour classes.
|
||||
*/
|
||||
public function resolutionSlaClasses(): string
|
||||
{
|
||||
if ($this->resolutionWasBreached()) {
|
||||
return 'border-red-300 bg-red-50 text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-100';
|
||||
}
|
||||
|
||||
if ($this->ticket->completed_at !== null) {
|
||||
return 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-100';
|
||||
}
|
||||
|
||||
if ($this->resolutionIsDueSoon()) {
|
||||
return 'border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100';
|
||||
}
|
||||
|
||||
return 'border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-100';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an SLA event name for the audit trail.
|
||||
*/
|
||||
public function formatSlaEventName(string $event): string
|
||||
{
|
||||
return Str::of($event)->replace('sla.', '')->replace('_', ' ')->title()->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a compact duration between two dates.
|
||||
*/
|
||||
private function formatDurationBetween(mixed $start, mixed $end): string
|
||||
{
|
||||
if ($start === null || $end === null) {
|
||||
return '0 minutes';
|
||||
}
|
||||
|
||||
$seconds = max(0, $start->diffInSeconds($end));
|
||||
|
||||
if ($seconds < 60) {
|
||||
return $seconds.' seconds';
|
||||
}
|
||||
|
||||
return CarbonInterval::seconds($seconds)
|
||||
->cascade()
|
||||
->forHumans([
|
||||
'short' => true,
|
||||
'parts' => 2,
|
||||
'join' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the first-response SLA was breached or is currently breached.
|
||||
*/
|
||||
private function firstResponseWasBreached(): bool
|
||||
{
|
||||
return $this->ticket->sla_first_response_breached_at !== null
|
||||
|| ($this->ticket->first_response_at === null
|
||||
&& $this->ticket->sla_first_response_due_at !== null
|
||||
&& now()->greaterThan($this->ticket->sla_first_response_due_at));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the first-response SLA is close to its deadline.
|
||||
*/
|
||||
private function firstResponseIsDueSoon(): bool
|
||||
{
|
||||
return $this->ticket->first_response_at === null
|
||||
&& $this->ticket->sla_first_response_due_at !== null
|
||||
&& now()->lessThanOrEqualTo($this->ticket->sla_first_response_due_at)
|
||||
&& now()->diffInMinutes($this->ticket->sla_first_response_due_at) <= 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the resolution SLA was breached or is currently breached.
|
||||
*/
|
||||
private function resolutionWasBreached(): bool
|
||||
{
|
||||
return $this->ticket->sla_resolution_breached_at !== null
|
||||
|| ($this->ticket->completed_at === null
|
||||
&& $this->ticket->sla_resolution_due_at !== null
|
||||
&& now()->greaterThan($this->ticket->sla_resolution_due_at));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the resolution SLA is close to its deadline.
|
||||
*/
|
||||
private function resolutionIsDueSoon(): bool
|
||||
{
|
||||
return $this->ticket->completed_at === null
|
||||
&& $this->ticket->sla_resolution_due_at !== null
|
||||
&& now()->lessThanOrEqualTo($this->ticket->sla_resolution_due_at)
|
||||
&& now()->diffInMinutes($this->ticket->sla_resolution_due_at) <= 60;
|
||||
}
|
||||
|
||||
private function hydrateTicketUsers(Ticket $ticket): void
|
||||
{
|
||||
$userIds = collect([$ticket->user_id, $ticket->agent_id])
|
||||
|
||||
Reference in New Issue
Block a user