From 37a116437e257fc99bbcd99b42b7b1ba33694acb Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Wed, 16 Sep 2026 11:59:22 +0100 Subject: [PATCH] new deployer command --- .../TicketCommentDeleteController.php | 84 ++++++------ .../Controllers/TicketTimeLogController.php | 114 +++++++++-------- Modules/Helpdesk/Livewire/SettingsManager.php | 121 ++++++++++-------- 3 files changed, 172 insertions(+), 147 deletions(-) diff --git a/Modules/Helpdesk/Http/Controllers/TicketCommentDeleteController.php b/Modules/Helpdesk/Http/Controllers/TicketCommentDeleteController.php index 4f8f578e..adc0a98b 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketCommentDeleteController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketCommentDeleteController.php @@ -25,51 +25,53 @@ final class TicketCommentDeleteController extends Controller 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(); - } + $this->deleteComment($comment, (int) $user->id); + $this->touchTicket($ticket); }); return redirect() ->route('helpdesk.tickets.show', $ticket) ->with('ticket-comment-deleted', 'Response deleted.'); } + + private function deleteComment(Comment $comment, int $userId): void + { + $updates = $this->tombstoneUpdates($comment, $userId); + + if ($updates === []) { + $comment->delete(); + + return; + } + + DB::table('helpdesk_comments')->where('id', $comment->id)->update($updates); + } + + /** @return array */ + private function tombstoneUpdates(Comment $comment, int $userId): array + { + $now = now(); + $candidates = [ + 'tombstoned_at' => $now, + 'deleted_by' => $userId, + 'delete_reason' => 'Deleted by MPBA platform administrator.', + 'sync_sequence' => ((int) ($comment->sync_sequence ?? 0)) + 1, + 'sync_hash' => hash('sha256', implode('|', [(string) ($comment->guid ?? $comment->id), 'deleted', $now->toISOString()])), + 'updated_at' => $now, + ]; + + return array_filter( + $candidates, + static fn (mixed $value, string $column): bool => Schema::hasColumn('helpdesk_comments', $column), + ARRAY_FILTER_USE_BOTH, + ); + } + + private function touchTicket(Ticket $ticket): void + { + if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) { + $ticket->forceFill(['last_activity_at' => now()])->save(); + } + } + } diff --git a/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php b/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php index 39148bd0..3e6b0571 100644 --- a/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php +++ b/Modules/Helpdesk/Http/Controllers/TicketTimeLogController.php @@ -35,62 +35,72 @@ final class TicketTimeLogController extends Controller $loggedAt = now(); DB::transaction(function () use ($ticket, $request, $minutes, $description, $loggedAt): void { - if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) { - $ticket->increment('time_spent_minutes', $minutes, [ - 'last_activity_at' => $loggedAt, - ]); - } elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) { - $ticket->forceFill(['last_activity_at' => $loggedAt])->save(); - } - - if (Schema::hasTable('helpdesk_time_logs')) { - DB::table('helpdesk_time_logs')->insert([ - 'ticket_id' => $ticket->getKey(), - 'user_id' => $request->user()->getKey(), - 'minutes' => $minutes, - 'description' => $description, - 'logged_at' => $loggedAt, - 'created_at' => $loggedAt, - 'updated_at' => $loggedAt, - ]); - } - - if (Schema::hasTable('helpdesk_ticket_time_logs')) { - $payload = [ - 'ticket_id' => $ticket->getKey(), - 'user_id' => $request->user()->getKey(), - 'minutes' => $minutes, - 'description' => $description, - 'created_at' => $loggedAt, - 'updated_at' => $loggedAt, - ]; - - if (Schema::hasColumn('helpdesk_ticket_time_logs', 'started_at')) { - $payload['started_at'] = $loggedAt->copy()->subMinutes($minutes); - } - - if (Schema::hasColumn('helpdesk_ticket_time_logs', 'ended_at')) { - $payload['ended_at'] = $loggedAt; - } - - if (Schema::hasColumn('helpdesk_ticket_time_logs', 'logged_at')) { - $payload['logged_at'] = $loggedAt; - } - - if (Schema::hasColumn('helpdesk_ticket_time_logs', 'duration_seconds')) { - $payload['duration_seconds'] = $minutes * 60; - } - - if (Schema::hasColumn('helpdesk_ticket_time_logs', 'seconds')) { - $payload['seconds'] = $minutes * 60; - } - - DB::table('helpdesk_ticket_time_logs')->insert($payload); - } + $this->updateTicketActivity($ticket, $minutes, $loggedAt); + $this->storeLegacyTimeLog($ticket, $request, $minutes, $description, $loggedAt); + $this->storeTicketTimeLog($ticket, $request, $minutes, $description, $loggedAt); }); return redirect() ->route('helpdesk.tickets.show', $ticket) ->with('ticket-action-updated', 'Time logged successfully.'); } + + private function updateTicketActivity(Ticket $ticket, int $minutes, $loggedAt): void + { + if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) { + $ticket->increment('time_spent_minutes', $minutes, ['last_activity_at' => $loggedAt]); + } elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) { + $ticket->forceFill(['last_activity_at' => $loggedAt])->save(); + } + } + + private function storeLegacyTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void + { + if (! Schema::hasTable('helpdesk_time_logs')) { + return; + } + + DB::table('helpdesk_time_logs')->insert([ + 'ticket_id' => $ticket->getKey(), + 'user_id' => $request->user()->getKey(), + 'minutes' => $minutes, + 'description' => $description, + 'logged_at' => $loggedAt, + 'created_at' => $loggedAt, + 'updated_at' => $loggedAt, + ]); + } + + private function storeTicketTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void + { + if (! Schema::hasTable('helpdesk_ticket_time_logs')) { + return; + } + + $payload = [ + 'ticket_id' => $ticket->getKey(), + 'user_id' => $request->user()->getKey(), + 'minutes' => $minutes, + 'description' => $description, + 'created_at' => $loggedAt, + 'updated_at' => $loggedAt, + ]; + + $optional = [ + 'started_at' => $loggedAt->copy()->subMinutes($minutes), + 'ended_at' => $loggedAt, + 'logged_at' => $loggedAt, + 'duration_seconds' => $minutes * 60, + 'seconds' => $minutes * 60, + ]; + + foreach ($optional as $column => $value) { + if (Schema::hasColumn('helpdesk_ticket_time_logs', $column)) { + $payload[$column] = $value; + } + } + + DB::table('helpdesk_ticket_time_logs')->insert($payload); + } + } diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index daf5efc4..e2030a13 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -31,6 +31,19 @@ use Modules\Helpdesk\Support\Color; final class SettingsManager extends Component { + private const RULE_MAX_40 = self::RULE_MAX_40; + private const RULE_MAX_80 = self::RULE_MAX_80; + private const RULE_MAX_100 = self::RULE_MAX_100; + private const RULE_MAX_120 = self::RULE_MAX_120; + private const RULE_MAX_160 = self::RULE_MAX_160; + private const RULE_MAX_180 = self::RULE_MAX_180; + private const RULE_MAX_220 = self::RULE_MAX_220; + private const RULE_MAX_500 = self::RULE_MAX_500; + private const RULE_MIN_0 = self::RULE_MIN_0; + private const RULE_TEAM_EXISTS = self::RULE_TEAM_EXISTS; + private const RULE_PROJECT_EXISTS = self::RULE_PROJECT_EXISTS; + private const RULE_CATEGORY_EXISTS = self::RULE_CATEGORY_EXISTS; + public string $section = 'statuses'; public int|string|null $default_helpdesk_company_id = null; @@ -126,10 +139,10 @@ final class SettingsManager extends Component public function saveStatus(): void { $data = $this->validate([ - 'statusForm.name' => ['required', 'string', 'max:80'], - 'statusForm.slug' => ['nullable', 'string', 'max:100'], + 'statusForm.name' => ['required', 'string', self::RULE_MAX_80], + 'statusForm.slug' => ['nullable', 'string', self::RULE_MAX_100], 'statusForm.color' => ['required', 'string', Color::validationRule()], - 'statusForm.sort_order' => ['required', 'integer', 'min:0'], + 'statusForm.sort_order' => ['required', 'integer', self::RULE_MIN_0], 'statusForm.is_closed' => ['boolean'], 'statusForm.is_default' => ['boolean'], ])['statusForm']; @@ -167,10 +180,10 @@ final class SettingsManager extends Component public function savePriority(): void { $data = $this->validate([ - 'priorityForm.name' => ['required', 'string', 'max:80'], - 'priorityForm.slug' => ['nullable', 'string', 'max:100'], + 'priorityForm.name' => ['required', 'string', self::RULE_MAX_80], + 'priorityForm.slug' => ['nullable', 'string', self::RULE_MAX_100], 'priorityForm.color' => ['required', 'string', Color::validationRule()], - 'priorityForm.sort_order' => ['required', 'integer', 'min:0'], + 'priorityForm.sort_order' => ['required', 'integer', self::RULE_MIN_0], 'priorityForm.is_default' => ['boolean'], ])['priorityForm']; @@ -209,7 +222,7 @@ final class SettingsManager extends Component $data = $this->validate([ 'categoryForm.company_id' => ['nullable', 'integer'], - 'categoryForm.name' => ['required', 'string', 'max:120'], + 'categoryForm.name' => ['required', 'string', self::RULE_MAX_120], 'categoryForm.slug' => ['nullable', 'string', 'max:140'], 'categoryForm.color' => ['required', 'string', Color::validationRule()], 'categoryForm.description' => ['nullable', 'string'], @@ -251,7 +264,7 @@ final class SettingsManager extends Component $data = $this->validate([ 'teamForm.company_id' => ['nullable', 'integer'], - 'teamForm.name' => ['required', 'string', 'max:120'], + 'teamForm.name' => ['required', 'string', self::RULE_MAX_120], 'teamForm.slug' => ['nullable', 'string', 'max:140'], 'teamForm.color' => ['required', 'string', Color::validationRule()], 'teamForm.description' => ['nullable', 'string'], @@ -293,9 +306,9 @@ final class SettingsManager extends Component $data = $this->validate([ 'projectForm.company_id' => ['nullable', 'integer'], - 'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], - 'projectForm.title' => ['required', 'string', 'max:160'], - 'projectForm.slug' => ['nullable', 'string', 'max:180'], + 'projectForm.team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS], + 'projectForm.title' => ['required', 'string', self::RULE_MAX_160], + 'projectForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'projectForm.color' => ['required', 'string', Color::validationRule()], 'projectForm.description' => ['nullable', 'string'], 'projectForm.is_default' => ['boolean'], @@ -335,12 +348,12 @@ final class SettingsManager extends Component $this->normalizeNullableFormValues('boardForm', ['owner_id']); $data = $this->validate([ - 'boardForm.name' => ['required', 'string', 'max:160'], - 'boardForm.slug' => ['nullable', 'string', 'max:180'], + 'boardForm.name' => ['required', 'string', self::RULE_MAX_160], + 'boardForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'boardForm.description' => ['nullable', 'string'], 'boardForm.owner_id' => ['nullable', 'integer', 'exists:users,id'], 'boardForm.is_default' => ['boolean'], - 'boardForm.sort_order' => ['required', 'integer', 'min:0'], + 'boardForm.sort_order' => ['required', 'integer', self::RULE_MIN_0], 'boardForm.status_ids' => ['array'], 'boardForm.status_ids.*' => ['integer', 'exists:helpdesk_statuses,id'], ])['boardForm']; @@ -403,12 +416,12 @@ final class SettingsManager extends Component $data = $this->validate([ 'repositoryForm.company_id' => ['nullable', 'integer'], - 'repositoryForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'], - 'repositoryForm.name' => ['required', 'string', 'max:160'], - 'repositoryForm.provider' => ['required', 'string', 'max:40'], - 'repositoryForm.remote_url' => ['required', 'string', 'max:500'], - 'repositoryForm.default_branch' => ['required', 'string', 'max:120'], - 'repositoryForm.webhook_secret' => ['nullable', 'string', 'max:120'], + 'repositoryForm.project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS], + 'repositoryForm.name' => ['required', 'string', self::RULE_MAX_160], + 'repositoryForm.provider' => ['required', 'string', self::RULE_MAX_40], + 'repositoryForm.remote_url' => ['required', 'string', self::RULE_MAX_500], + 'repositoryForm.default_branch' => ['required', 'string', self::RULE_MAX_120], + 'repositoryForm.webhook_secret' => ['nullable', 'string', self::RULE_MAX_120], ])['repositoryForm']; $data['provider'] = strtolower(trim((string) $data['provider'])); @@ -440,14 +453,14 @@ final class SettingsManager extends Component $this->normalizeNullableFormValues('workflowForm', ['text_body', 'cc', 'bcc']); $data = $this->validate([ - 'workflowForm.name' => ['required', 'string', 'max:160'], - 'workflowForm.event' => ['required', 'string', 'max:80'], - 'workflowForm.subject' => ['required', 'string', 'max:220'], + 'workflowForm.name' => ['required', 'string', self::RULE_MAX_160], + 'workflowForm.event' => ['required', 'string', self::RULE_MAX_80], + 'workflowForm.subject' => ['required', 'string', self::RULE_MAX_220], 'workflowForm.html_body' => ['required', 'string'], 'workflowForm.text_body' => ['nullable', 'string'], - 'workflowForm.to_role' => ['required', 'string', 'max:40'], - 'workflowForm.cc' => ['nullable', 'string', 'max:500'], - 'workflowForm.bcc' => ['nullable', 'string', 'max:500'], + 'workflowForm.to_role' => ['required', 'string', self::RULE_MAX_40], + 'workflowForm.cc' => ['nullable', 'string', self::RULE_MAX_500], + 'workflowForm.bcc' => ['nullable', 'string', self::RULE_MAX_500], 'workflowForm.is_active' => ['boolean'], ])['workflowForm']; @@ -479,15 +492,15 @@ final class SettingsManager extends Component $data = $this->validate([ 'cannedForm.company_id' => ['nullable', 'integer'], - 'cannedForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], - 'cannedForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], - 'cannedForm.name' => ['required', 'string', 'max:160'], - 'cannedForm.slug' => ['nullable', 'string', 'max:180'], - 'cannedForm.visibility' => ['required', 'string', 'max:40'], - 'cannedForm.subject' => ['nullable', 'string', 'max:220'], + 'cannedForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS], + 'cannedForm.team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS], + 'cannedForm.name' => ['required', 'string', self::RULE_MAX_160], + 'cannedForm.slug' => ['nullable', 'string', self::RULE_MAX_180], + 'cannedForm.visibility' => ['required', 'string', self::RULE_MAX_40], + 'cannedForm.subject' => ['nullable', 'string', self::RULE_MAX_220], 'cannedForm.body' => ['required', 'string'], 'cannedForm.is_active' => ['boolean'], - 'cannedForm.sort_order' => ['required', 'integer', 'min:0'], + 'cannedForm.sort_order' => ['required', 'integer', self::RULE_MIN_0], ])['cannedForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); $this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data); @@ -519,9 +532,9 @@ final class SettingsManager extends Component $data = $this->validate([ 'slaForm.company_id' => ['nullable', 'integer'], 'slaForm.priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'], - 'slaForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], - 'slaForm.name' => ['required', 'string', 'max:160'], - 'slaForm.slug' => ['nullable', 'string', 'max:180'], + 'slaForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS], + 'slaForm.name' => ['required', 'string', self::RULE_MAX_160], + 'slaForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'slaForm.first_response_minutes' => ['required', 'integer', 'min:1'], 'slaForm.resolution_minutes' => ['required', 'integer', 'min:1'], 'slaForm.business_hours_only' => ['boolean'], @@ -560,20 +573,20 @@ final class SettingsManager extends Component $data = $this->validate([ 'automationForm.company_id' => ['nullable', 'integer'], - 'automationForm.name' => ['required', 'string', 'max:160'], - 'automationForm.slug' => ['nullable', 'string', 'max:180'], + 'automationForm.name' => ['required', 'string', self::RULE_MAX_160], + 'automationForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'automationForm.match_sender' => ['nullable', 'string', 'max:190'], - 'automationForm.match_subject' => ['nullable', 'string', 'max:220'], + 'automationForm.match_subject' => ['nullable', 'string', self::RULE_MAX_220], 'automationForm.match_mailbox' => ['nullable', 'string', 'max:190'], - 'automationForm.match_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], + 'automationForm.match_category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS], 'automationForm.set_priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'], - 'automationForm.set_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], - 'automationForm.set_team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], - 'automationForm.set_project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'], + 'automationForm.set_category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS], + 'automationForm.set_team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS], + 'automationForm.set_project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS], 'automationForm.set_status_id' => ['nullable', 'integer', 'exists:helpdesk_statuses,id'], 'automationForm.set_agent_id' => ['nullable', 'integer', 'exists:users,id'], 'automationForm.is_active' => ['boolean'], - 'automationForm.sort_order' => ['required', 'integer', 'min:0'], + 'automationForm.sort_order' => ['required', 'integer', self::RULE_MIN_0], ])['automationForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); $this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data); @@ -604,11 +617,11 @@ final class SettingsManager extends Component $data = $this->validate([ 'productForm.company_id' => ['nullable', 'integer'], - 'productForm.name' => ['required', 'string', 'max:160'], - 'productForm.slug' => ['nullable', 'string', 'max:180'], + 'productForm.name' => ['required', 'string', self::RULE_MAX_160], + 'productForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'productForm.type' => ['required', 'string', 'max:60'], - 'productForm.version' => ['nullable', 'string', 'max:80'], - 'productForm.release_channel' => ['nullable', 'string', 'max:80'], + 'productForm.version' => ['nullable', 'string', self::RULE_MAX_80], + 'productForm.release_channel' => ['nullable', 'string', self::RULE_MAX_80], 'productForm.description' => ['nullable', 'string'], 'productForm.is_active' => ['boolean'], ])['productForm']; @@ -641,8 +654,8 @@ final class SettingsManager extends Component $data = $this->validate([ 'tagForm.company_id' => ['nullable', 'integer'], - 'tagForm.name' => ['required', 'string', 'max:80'], - 'tagForm.slug' => ['nullable', 'string', 'max:100'], + 'tagForm.name' => ['required', 'string', self::RULE_MAX_80], + 'tagForm.slug' => ['nullable', 'string', self::RULE_MAX_100], 'tagForm.color' => ['required', 'string', Color::validationRule()], ])['tagForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); @@ -675,10 +688,10 @@ final class SettingsManager extends Component $data = $this->validate([ 'checklistTemplateForm.company_id' => ['nullable', 'integer'], - 'checklistTemplateForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'], - 'checklistTemplateForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'], - 'checklistTemplateForm.name' => ['required', 'string', 'max:160'], - 'checklistTemplateForm.slug' => ['nullable', 'string', 'max:180'], + 'checklistTemplateForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS], + 'checklistTemplateForm.project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS], + 'checklistTemplateForm.name' => ['required', 'string', self::RULE_MAX_160], + 'checklistTemplateForm.slug' => ['nullable', 'string', self::RULE_MAX_180], 'checklistTemplateForm.items_text' => ['required', 'string'], 'checklistTemplateForm.is_active' => ['boolean'], ])['checklistTemplateForm'];