From a8cbb8e2ac87260f78a599699f54500f6ed545bf Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 23 Jun 2026 15:32:05 +0100 Subject: [PATCH] standard colours --- ...0000_normalise_helpdesk_lookup_colours.php | 68 +++++++++++++ .../Seeders/HelpdeskDatabaseSeeder.php | 14 +-- Modules/Helpdesk/Livewire/SettingsManager.php | 39 +++++--- .../views/components/colour-badge.blade.php | 10 ++ .../views/components/colour-select.blade.php | 34 +++++++ .../views/components/lookup-table.blade.php | 5 +- .../views/components/project-table.blade.php | 9 +- .../views/components/team-table.blade.php | 9 +- .../views/livewire/dashboard-widget.blade.php | 5 +- .../views/livewire/settings-manager.blade.php | 12 +-- .../views/livewire/ticket-index.blade.php | 6 +- .../views/livewire/ticket-show.blade.php | 4 +- Modules/Helpdesk/Support/Color.php | 97 ++++++++++++++++--- .../Tests/Concerns/BuildsHelpdeskFixtures.php | 4 +- .../Tests/Feature/SettingsCrudTest.php | 22 ++++- .../Tests/Feature/SettingsCrudTest.php.tmp | 0 .../Helpdesk/Tests/Unit/ColorPaletteTest.php | 33 +++++++ Modules/Helpdesk/changelog.yaml | 13 +++ Modules/Helpdesk/module.json | 2 +- 19 files changed, 330 insertions(+), 56 deletions(-) create mode 100644 Modules/Helpdesk/Database/Migrations/2026_06_23_150000_normalise_helpdesk_lookup_colours.php create mode 100644 Modules/Helpdesk/Resources/views/components/colour-badge.blade.php create mode 100644 Modules/Helpdesk/Resources/views/components/colour-select.blade.php create mode 100644 Modules/Helpdesk/Tests/Feature/SettingsCrudTest.php.tmp create mode 100644 Modules/Helpdesk/Tests/Unit/ColorPaletteTest.php diff --git a/Modules/Helpdesk/Database/Migrations/2026_06_23_150000_normalise_helpdesk_lookup_colours.php b/Modules/Helpdesk/Database/Migrations/2026_06_23_150000_normalise_helpdesk_lookup_colours.php new file mode 100644 index 00000000..459ff371 --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_06_23_150000_normalise_helpdesk_lookup_colours.php @@ -0,0 +1,68 @@ +lookupTables() as $table) { + if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'color')) { + continue; + } + + foreach ($this->colourMap() as $legacy => $approved) { + DB::table($table)->where('color', $legacy)->update(['color' => $approved]); + } + } + } + + /** + * The migration is intentionally one-way because approved colours are the new baseline. + */ + public function down(): void + { + // No-op: do not reintroduce legacy colour aliases. + } + + /** + * @return list + */ + private function lookupTables(): array + { + return [ + 'helpdesk_statuses', + 'helpdesk_priorities', + 'helpdesk_categories', + 'helpdesk_teams', + 'helpdesk_projects', + 'helpdesk_tags', + ]; + } + + /** + * @return array + */ + private function colourMap(): array + { + return [ + 'emerald' => 'green', + 'lime' => 'green', + 'rose' => 'red', + 'yellow' => 'amber', + 'orange' => 'amber', + 'sky' => 'blue', + 'cyan' => 'blue', + 'violet' => 'purple', + 'slate' => 'gray', + 'zinc' => 'gray', + ]; + } +}; diff --git a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php index 86d8c758..25ebe9d5 100644 --- a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php +++ b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php @@ -45,12 +45,12 @@ final class HelpdeskDatabaseSeeder extends Seeder Status::query()->where('is_default', true)->update(['is_default' => false]); foreach ([ - ['name' => 'New', 'slug' => 'new', 'color' => 'sky', 'sort_order' => 5, 'is_closed' => false, 'is_default' => true], + ['name' => 'New', 'slug' => 'new', 'color' => 'blue', 'sort_order' => 5, 'is_closed' => false, 'is_default' => true], ['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false, 'is_default' => false], ['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false, 'is_default' => false], - ['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'rose', 'sort_order' => 30, 'is_closed' => false, 'is_default' => false], - ['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'violet', 'sort_order' => 40, 'is_closed' => false, 'is_default' => false], - ['name' => 'Closed', 'slug' => 'closed', 'color' => 'emerald', 'sort_order' => 90, 'is_closed' => true, 'is_default' => false], + ['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'red', 'sort_order' => 30, 'is_closed' => false, 'is_default' => false], + ['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'purple', 'sort_order' => 40, 'is_closed' => false, 'is_default' => false], + ['name' => 'Closed', 'slug' => 'closed', 'color' => 'green', 'sort_order' => 90, 'is_closed' => true, 'is_default' => false], ] as $row) { Status::query()->updateOrCreate(['slug' => $row['slug']], $row); } @@ -66,7 +66,7 @@ final class HelpdeskDatabaseSeeder extends Seeder foreach ([ ['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 10, 'is_default' => false], ['name' => 'Medium', 'slug' => 'medium', 'color' => 'amber', 'sort_order' => 20, 'is_default' => true], - ['name' => 'Low', 'slug' => 'low', 'color' => 'emerald', 'sort_order' => 30, 'is_default' => false], + ['name' => 'Low', 'slug' => 'low', 'color' => 'green', 'sort_order' => 30, 'is_default' => false], ] as $row) { Priority::query()->updateOrCreate(['slug' => $row['slug']], $row); } @@ -83,9 +83,9 @@ final class HelpdeskDatabaseSeeder extends Seeder ->update(['is_default' => false]); foreach ([ ['name' => 'Development', 'color' => 'indigo', 'description' => 'Software changes, enhancements and development work.', 'is_default' => true], - ['name' => 'Systems Deployment', 'color' => 'cyan', 'description' => 'Deployment, release, infrastructure and environment requests.', 'is_default' => false], + ['name' => 'Systems Deployment', 'color' => 'blue', 'description' => 'Deployment, release, infrastructure and environment requests.', 'is_default' => false], ['name' => 'Concepts', 'color' => 'purple', 'description' => 'Ideas, concepts, design discussion and future improvements.', 'is_default' => false], - ['name' => 'Bug', 'color' => 'rose', 'description' => 'Defects, regressions and incorrect behaviour.', 'is_default' => false], + ['name' => 'Bug', 'color' => 'red', 'description' => 'Defects, regressions and incorrect behaviour.', 'is_default' => false], ] as $row) { Category::query()->updateOrCreate( ['company_id' => null, 'slug' => Str::slug($row['name'])], diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index 49118051..34213ba8 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -11,6 +11,7 @@ use Illuminate\Database\QueryException; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; use Livewire\Component; use Modules\Helpdesk\Models\AutomationRule; @@ -27,6 +28,7 @@ use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Tag; use Modules\Helpdesk\Models\Team; use Modules\Helpdesk\Models\WorkflowTemplate; +use Modules\Helpdesk\Support\Color; final class SettingsManager extends Component { @@ -35,10 +37,10 @@ final class SettingsManager extends Component public int|string|null $default_helpdesk_company_id = null; /** @var array */ - public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; + public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; /** @var array */ - public array $priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_default' => false]; + public array $priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false]; /** @var array */ public array $categoryForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false]; @@ -68,7 +70,7 @@ final class SettingsManager extends Component public array $productForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true]; /** @var array */ - public array $tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc']; + public array $tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'gray']; /** @var array */ public array $checklistTemplateForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true]; @@ -80,8 +82,8 @@ final class SettingsManager extends Component { $this->section = $this->validSection((string) request()->query('section', $this->section)); $this->loadDefaultHelpdeskCompany(); - $this->statusForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; - $this->priorityForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_default' => false]; + $this->statusForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; + $this->priorityForm += ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false]; $this->categoryForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false]; $this->teamForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'is_default' => false, 'archived_at' => null]; $this->projectForm += ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null]; @@ -92,7 +94,7 @@ final class SettingsManager extends Component $this->slaForm += ['id' => null, 'company_id' => null, 'priority_id' => null, 'category_id' => null, 'name' => '', 'slug' => '', 'first_response_minutes' => 240, 'resolution_minutes' => 2880, 'business_hours_only' => false, 'is_default' => false, 'is_active' => true]; $this->automationForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'match_sender' => '', 'match_subject' => '', 'match_mailbox' => '', 'match_category_id' => null, 'set_priority_id' => null, 'set_category_id' => null, 'set_team_id' => null, 'set_project_id' => null, 'set_status_id' => null, 'set_agent_id' => null, 'is_active' => true, 'sort_order' => 0]; $this->productForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'type' => 'service', 'version' => '', 'release_channel' => '', 'description' => '', 'is_active' => true]; - $this->tagForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc']; + $this->tagForm += ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'gray']; $this->checklistTemplateForm += ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true]; } @@ -127,7 +129,7 @@ final class SettingsManager extends Component $data = $this->validate([ 'statusForm.name' => ['required', 'string', 'max:80'], 'statusForm.slug' => ['nullable', 'string', 'max:100'], - 'statusForm.color' => ['required', 'string', 'max:30'], + 'statusForm.color' => ['required', 'string', Rule::in(Color::keys())], 'statusForm.sort_order' => ['required', 'integer', 'min:0'], 'statusForm.is_closed' => ['boolean'], 'statusForm.is_default' => ['boolean'], @@ -148,6 +150,7 @@ final class SettingsManager extends Component public function editStatus(int $id): void { $this->statusForm = Status::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_closed', 'is_default']); + $this->statusForm['color'] = Color::normalise($this->statusForm['color'] ?? 'gray'); $this->section = 'statuses'; } @@ -159,7 +162,7 @@ final class SettingsManager extends Component public function resetStatusForm(): void { - $this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; + $this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_closed' => false, 'is_default' => false]; } public function savePriority(): void @@ -167,7 +170,7 @@ final class SettingsManager extends Component $data = $this->validate([ 'priorityForm.name' => ['required', 'string', 'max:80'], 'priorityForm.slug' => ['nullable', 'string', 'max:100'], - 'priorityForm.color' => ['required', 'string', 'max:30'], + 'priorityForm.color' => ['required', 'string', Rule::in(Color::keys())], 'priorityForm.sort_order' => ['required', 'integer', 'min:0'], 'priorityForm.is_default' => ['boolean'], ])['priorityForm']; @@ -186,6 +189,7 @@ final class SettingsManager extends Component public function editPriority(int $id): void { $this->priorityForm = Priority::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_default']); + $this->priorityForm['color'] = Color::normalise($this->priorityForm['color'] ?? 'gray'); $this->section = 'priorities'; } @@ -197,7 +201,7 @@ final class SettingsManager extends Component public function resetPriorityForm(): void { - $this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_default' => false]; + $this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'gray', 'sort_order' => 0, 'is_default' => false]; } public function saveCategory(): void @@ -208,7 +212,7 @@ final class SettingsManager extends Component 'categoryForm.company_id' => ['nullable', 'integer'], 'categoryForm.name' => ['required', 'string', 'max:120'], 'categoryForm.slug' => ['nullable', 'string', 'max:140'], - 'categoryForm.color' => ['required', 'string', 'max:30'], + 'categoryForm.color' => ['required', 'string', Rule::in(Color::keys())], 'categoryForm.description' => ['nullable', 'string'], 'categoryForm.is_default' => ['boolean'], ])['categoryForm']; @@ -227,6 +231,7 @@ final class SettingsManager extends Component public function editCategory(int $id): void { $this->categoryForm = Category::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color', 'description', 'is_default']); + $this->categoryForm['color'] = Color::normalise($this->categoryForm['color'] ?? 'blue'); $this->section = 'categories'; } @@ -249,7 +254,7 @@ final class SettingsManager extends Component 'teamForm.company_id' => ['nullable', 'integer'], 'teamForm.name' => ['required', 'string', 'max:120'], 'teamForm.slug' => ['nullable', 'string', 'max:140'], - 'teamForm.color' => ['required', 'string', 'max:30'], + 'teamForm.color' => ['required', 'string', Rule::in(Color::keys())], 'teamForm.description' => ['nullable', 'string'], 'teamForm.is_default' => ['boolean'], ])['teamForm']; @@ -268,6 +273,7 @@ final class SettingsManager extends Component public function editTeam(int $id): void { $this->teamForm = Team::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color', 'description', 'is_default', 'archived_at']); + $this->teamForm['color'] = Color::normalise($this->teamForm['color'] ?? 'indigo'); $this->section = 'teams'; } @@ -291,7 +297,7 @@ final class SettingsManager extends Component 'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], 'projectForm.title' => ['required', 'string', 'max:160'], 'projectForm.slug' => ['nullable', 'string', 'max:180'], - 'projectForm.color' => ['required', 'string', 'max:30'], + 'projectForm.color' => ['required', 'string', Rule::in(Color::keys())], 'projectForm.description' => ['nullable', 'string'], 'projectForm.is_default' => ['boolean'], ])['projectForm']; @@ -310,6 +316,7 @@ final class SettingsManager extends Component public function editProject(int $id): void { $this->projectForm = Project::query()->findOrFail($id)->only(['id', 'company_id', 'team_id', 'title', 'slug', 'color', 'description', 'is_default', 'archived_at']); + $this->projectForm['color'] = Color::normalise($this->projectForm['color'] ?? 'blue'); $this->section = 'projects'; } @@ -637,7 +644,7 @@ final class SettingsManager extends Component 'tagForm.company_id' => ['nullable', 'integer'], 'tagForm.name' => ['required', 'string', 'max:80'], 'tagForm.slug' => ['nullable', 'string', 'max:100'], - 'tagForm.color' => ['required', 'string', 'max:30'], + 'tagForm.color' => ['required', 'string', Rule::in(Color::keys())], ])['tagForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); $this->persistSettingModel(Tag::class, $this->tagForm['id'], $data); @@ -648,6 +655,7 @@ final class SettingsManager extends Component public function editTag(int $id): void { $this->tagForm = Tag::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color']); + $this->tagForm['color'] = Color::normalise($this->tagForm['color'] ?? 'gray'); $this->section = 'tags'; } @@ -659,7 +667,7 @@ final class SettingsManager extends Component public function resetTagForm(): void { - $this->tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc']; + $this->tagForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'gray']; } public function saveChecklistTemplate(): void @@ -765,6 +773,7 @@ final class SettingsManager extends Component 'products' => Product::query()->withCount('tickets')->orderBy('name')->get(), 'tags' => Tag::query()->orderBy('name')->get(), 'checklistTemplates' => ChecklistTemplate::query()->orderBy('name')->get(), + 'colourOptions' => Color::options(), ])->layout('layouts.app'); } diff --git a/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php b/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php new file mode 100644 index 00000000..78c488b6 --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php @@ -0,0 +1,10 @@ +@props([ + 'color' => 'gray', +]) + +class('inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold text-white shadow-sm ring-1 ring-white/20') }} + style="background-color: {{ \Modules\Helpdesk\Support\Color::css($color) }};" +> + {{ $slot }} + diff --git a/Modules/Helpdesk/Resources/views/components/colour-select.blade.php b/Modules/Helpdesk/Resources/views/components/colour-select.blade.php new file mode 100644 index 00000000..cc3e9c6a --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/colour-select.blade.php @@ -0,0 +1,34 @@ +@props([ + 'label' => 'Badge colour', + 'model', + 'options' => [], +]) + +
class('space-y-2') }}> + + + + +
+ @foreach ($options as $key => $option) + + {{ $option['name'] }} + + @endforeach +
+
diff --git a/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php b/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php index b46647c5..582c3d4d 100644 --- a/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php +++ b/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php @@ -17,7 +17,10 @@ {{ $item->name }} {{ $item->slug }} - {{ $item->color }} + + + {{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }} + diff --git a/Modules/Helpdesk/Resources/views/components/project-table.blade.php b/Modules/Helpdesk/Resources/views/components/project-table.blade.php index b4d2d896..4a8f7d9d 100644 --- a/Modules/Helpdesk/Resources/views/components/project-table.blade.php +++ b/Modules/Helpdesk/Resources/views/components/project-table.blade.php @@ -6,6 +6,7 @@ Project Company Team + Colour Default Tickets Actions @@ -17,6 +18,12 @@ {{ $item->title }} {{ $item->company?->name ?? 'Global' }} {{ $item->team?->name ?? 'No team' }} + + + + {{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }} + + {{ $item->is_default ?? false ? 'Default' : 'No' }} @@ -32,7 +39,7 @@ @empty - No projects yet. + No projects yet. @endforelse diff --git a/Modules/Helpdesk/Resources/views/components/team-table.blade.php b/Modules/Helpdesk/Resources/views/components/team-table.blade.php index 7f43abce..0479aea5 100644 --- a/Modules/Helpdesk/Resources/views/components/team-table.blade.php +++ b/Modules/Helpdesk/Resources/views/components/team-table.blade.php @@ -5,6 +5,7 @@ Team Company + Colour Default Tickets Actions @@ -15,6 +16,12 @@ {{ $item->name }} {{ $item->company?->name ?? 'Global' }} + + + + {{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }} + + {{ $item->is_default ?? false ? 'Default' : 'No' }} @@ -30,7 +37,7 @@ @empty - No teams yet. + No teams yet. @endforelse diff --git a/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php b/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php index eb2f38c0..dfb972c5 100644 --- a/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php @@ -95,8 +95,9 @@
{{ $ticket->subject }} - - {{ $ticket->priority->name }} + + {{ $ticket->priority->name }} +
{{ $ticket->reference }} · {{ $ticket->status->name }}
diff --git a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php index 5ceb8ea8..e3f892fa 100644 --- a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php @@ -143,7 +143,7 @@
- +