standard colours

This commit is contained in:
Mark Gregory
2026-06-23 15:32:05 +01:00
parent 3be4ff590f
commit a8cbb8e2ac
19 changed files with 330 additions and 56 deletions
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Normalise legacy Flux/Tailwind colour names to the approved WMS palette.
*/
public function up(): void
{
foreach ($this->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<string>
*/
private function lookupTables(): array
{
return [
'helpdesk_statuses',
'helpdesk_priorities',
'helpdesk_categories',
'helpdesk_teams',
'helpdesk_projects',
'helpdesk_tags',
];
}
/**
* @return array<string, string>
*/
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',
];
}
};
@@ -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'])],
+24 -15
View File
@@ -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<string, mixed> */
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<string, mixed> */
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<string, mixed> */
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<string, mixed> */
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<string, mixed> */
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');
}
@@ -0,0 +1,10 @@
@props([
'color' => 'gray',
])
<span
{{ $attributes->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 }}
</span>
@@ -0,0 +1,34 @@
@props([
'label' => 'Badge colour',
'model',
'options' => [],
])
<div {{ $attributes->class('space-y-2') }}>
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">
{{ $label }}
</label>
<select
wire:model="{{ $model }}"
class="block h-10 w-full rounded-xl border border-zinc-300 bg-white px-3 text-sm font-semibold text-zinc-900 shadow-sm outline-none transition-colors duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
>
@foreach ($options as $key => $option)
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
● {{ $option['name'] }} — {{ $option['hex'] }}
</option>
@endforeach
</select>
<div class="flex flex-wrap gap-1.5" aria-label="Available Helpdesk colours">
@foreach ($options as $key => $option)
<span
title="{{ $option['name'] }} {{ $option['hex'] }}"
class="inline-flex h-5 min-w-5 items-center justify-center rounded-full border border-white/70 shadow-sm ring-1 ring-zinc-900/10"
style="background-color: {{ $option['hex'] }}"
>
<span class="sr-only">{{ $option['name'] }}</span>
</span>
@endforeach
</div>
</div>
@@ -17,7 +17,10 @@
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-4 py-3 font-mono text-xs text-zinc-500">{{ $item->slug }}</td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->color }}">{{ $item->color }}</flux:badge>
<span class="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<span class="h-3 w-3 rounded-full" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($item->color) }}"></span>
{{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }}
</span>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
@@ -6,6 +6,7 @@
<th class="px-4 py-3">Project</th>
<th class="px-4 py-3">Company</th>
<th class="px-4 py-3">Team</th>
<th class="px-4 py-3">Colour</th>
<th class="px-4 py-3">Default</th>
<th class="px-4 py-3">Tickets</th>
<th class="px-4 py-3 text-right">Actions</th>
@@ -17,6 +18,12 @@
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->title }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->team?->name ?? 'No team' }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<span class="h-3 w-3 rounded-full" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($item->color) }}"></span>
{{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }}
</span>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
@@ -32,7 +39,7 @@
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-10 text-center text-zinc-500">No projects yet.</td>
<td colspan="7" class="px-4 py-10 text-center text-zinc-500">No projects yet.</td>
</tr>
@endforelse
</tbody>
@@ -5,6 +5,7 @@
<tr>
<th class="px-4 py-3">Team</th>
<th class="px-4 py-3">Company</th>
<th class="px-4 py-3">Colour</th>
<th class="px-4 py-3">Default</th>
<th class="px-4 py-3">Tickets</th>
<th class="px-4 py-3 text-right">Actions</th>
@@ -15,6 +16,12 @@
<tr>
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
<span class="h-3 w-3 rounded-full" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($item->color) }}"></span>
{{ \Illuminate\Support\Str::of(\Modules\Helpdesk\Support\Color::normalise($item->color))->headline() }}
</span>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
@@ -30,7 +37,7 @@
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-10 text-center text-zinc-500">No teams yet.</td>
<td colspan="6" class="px-4 py-10 text-center text-zinc-500">No teams yet.</td>
</tr>
@endforelse
</tbody>
@@ -95,8 +95,9 @@
<div class="flex items-center justify-between gap-3">
<span
class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</span>
<flux:badge color="{{ $ticket->priority->color }}" size="sm">
{{ $ticket->priority->name }}</flux:badge>
<x-helpdesk::colour-badge :color="$ticket->priority->color">
{{ $ticket->priority->name }}
</x-helpdesk::colour-badge>
</div>
<div class="mt-1 text-xs text-zinc-500">{{ $ticket->reference }} · {{ $ticket->status->name }}
</div>
@@ -143,7 +143,7 @@
<form wire:submit.prevent="saveStatus" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="statusForm.name" label="Name" />
<flux:input wire:model="statusForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="statusForm.color" label="Badge colour" />
<x-helpdesk::colour-select model="statusForm.color" label="Badge colour" :options="$colourOptions" />
<flux:input wire:model="statusForm.sort_order" label="Sort" type="number" min="0" />
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input
type="checkbox" wire:model="statusForm.is_closed" class="rounded border-zinc-300"> Closed
@@ -174,7 +174,7 @@
<form wire:submit.prevent="savePriority" class="grid gap-4 md:grid-cols-4">
<flux:input wire:model="priorityForm.name" label="Name" />
<flux:input wire:model="priorityForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="priorityForm.color" label="Badge colour" />
<x-helpdesk::colour-select model="priorityForm.color" label="Badge colour" :options="$colourOptions" />
<flux:input wire:model="priorityForm.sort_order" label="Sort" type="number" min="0" />
<label class="flex items-end gap-2 pb-2 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
<input type="checkbox" wire:model="priorityForm.is_default"
@@ -208,7 +208,7 @@
</flux:select>
<flux:input wire:model="categoryForm.name" label="Name" />
<flux:input wire:model="categoryForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="categoryForm.color" label="Badge colour" />
<x-helpdesk::colour-select model="categoryForm.color" label="Badge colour" :options="$colourOptions" />
<div class="md:col-span-4">
<flux:textarea wire:model="categoryForm.description" label="Description" rows="3" />
</div>
@@ -245,7 +245,7 @@
</flux:select>
<flux:input wire:model="teamForm.name" label="Name" />
<flux:input wire:model="teamForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="teamForm.color" label="Badge colour" />
<x-helpdesk::colour-select model="teamForm.color" label="Badge colour" :options="$colourOptions" />
<div class="md:col-span-4">
<flux:textarea wire:model="teamForm.description" label="Description" rows="3" />
</div>
@@ -288,7 +288,7 @@
</flux:select>
<flux:input wire:model="projectForm.title" label="Title" />
<flux:input wire:model="projectForm.slug" label="Slug" placeholder="auto-generated" />
<flux:input wire:model="projectForm.color" label="Badge colour" />
<x-helpdesk::colour-select model="projectForm.color" label="Badge colour" :options="$colourOptions" />
<div class="md:col-span-5">
<flux:textarea wire:model="projectForm.description" label="Description" rows="3" />
</div>
@@ -868,7 +868,7 @@
<form wire:submit.prevent="saveTag" class="grid gap-4 md:grid-cols-5">
<flux:input wire:model="tagForm.name" label="Name" />
<flux:input wire:model="tagForm.slug" label="Slug" />
<flux:input wire:model="tagForm.color" label="Colour" />
<x-helpdesk::colour-select model="tagForm.color" label="Colour" :options="$colourOptions" />
<div class="md:col-span-5 flex gap-2">
<flux:button type="submit" variant="primary"
class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag
@@ -92,12 +92,10 @@
</div>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}
</flux:badge>
<x-helpdesk::colour-badge :color="$ticket->status->color">{{ $ticket->status->name }}</x-helpdesk::colour-badge>
</td>
<td class="px-4 py-3">
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}
</flux:badge>
<x-helpdesk::colour-badge :color="$ticket->priority->color">{{ $ticket->priority->name }}</x-helpdesk::colour-badge>
</td>
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
{{ $ticket->agent?->name ?? 'Unassigned' }}</td>
@@ -23,8 +23,8 @@
<div class="min-w-0">
<div class="mb-2 flex flex-wrap items-center gap-2">
<span class="font-mono text-xs text-blue-100">{{ $ticket->reference }}</span>
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
<x-helpdesk::colour-badge :color="$ticket->status->color">{{ $ticket->status->name }}</x-helpdesk::colour-badge>
<x-helpdesk::colour-badge :color="$ticket->priority->color">{{ $ticket->priority->name }}</x-helpdesk::colour-badge>
</div>
<h1 class="truncate text-2xl font-semibold tracking-tight">{{ $ticket->subject }}</h1>
+85 -12
View File
@@ -6,19 +6,92 @@ namespace Modules\Helpdesk\Support;
final class Color
{
/**
* Standard approved Helpdesk badge colours.
*
* @var array<string, string>
*/
public const PALETTE = [
'green' => '#16a34a',
'red' => '#dc2626',
'amber' => '#d97706',
'blue' => '#2563eb',
'indigo' => '#4f46e5',
'purple' => '#7c3aed',
'gray' => '#6b7280',
];
/**
* Legacy Flux/Tailwind colour aliases normalised into the WMS palette.
*
* @var array<string, string>
*/
private const ALIASES = [
'emerald' => 'green',
'lime' => 'green',
'rose' => 'red',
'yellow' => 'amber',
'orange' => 'amber',
'sky' => 'blue',
'cyan' => 'blue',
'violet' => 'purple',
'slate' => 'gray',
'zinc' => 'gray',
'' => 'gray',
];
/**
* Resolve a stored colour key or supported hex value into a CSS colour.
*/
public static function css(?string $color): string
{
return match ($color) {
'red', 'rose' => '#e11d48',
'amber', 'yellow' => '#d97706',
'emerald', 'green' => '#059669',
'blue' => '#2563eb',
'sky', 'cyan' => '#0284c7',
'indigo' => '#4f46e5',
'violet', 'purple' => '#7c3aed',
'slate' => '#334155',
'zinc', null, '' => '#52525b',
default => str_starts_with((string) $color, '#') ? (string) $color : '#52525b',
};
$resolved = self::normalise($color);
if (str_starts_with($resolved, '#')) {
return $resolved;
}
return self::PALETTE[$resolved] ?? self::PALETTE['gray'];
}
/**
* Return approved palette options for settings dropdowns.
*
* @return array<string, array{name: string, hex: string}>
*/
public static function options(): array
{
return collect(self::PALETTE)
->mapWithKeys(fn (string $hex, string $name): array => [
$name => [
'name' => str($name)->headline()->toString(),
'hex' => $hex,
],
])
->all();
}
/**
* Return approved palette keys for validation rules.
*
* @return list<string>
*/
public static function keys(): array
{
return array_keys(self::PALETTE);
}
/**
* Normalise legacy aliases and supported hex colours.
*/
public static function normalise(?string $color): string
{
$value = strtolower(trim((string) $color));
if (preg_match('/^#[a-f0-9]{6}$/', $value) === 1) {
return $value;
}
return self::ALIASES[$value] ?? $value;
}
}
@@ -63,7 +63,7 @@ trait BuildsHelpdeskFixtures
return $this->ticketStatus(array_merge([
'name' => 'Closed',
'slug' => fake()->unique()->slug(2),
'color' => 'emerald',
'color' => 'green',
'sort_order' => 99,
'is_closed' => true,
], $attributes));
@@ -165,7 +165,7 @@ trait BuildsHelpdeskFixtures
['slug' => 'closed'],
[
'name' => 'Closed',
'color' => 'emerald',
'color' => 'green',
'sort_order' => 99,
'is_closed' => true,
],
@@ -97,7 +97,7 @@ final class SettingsCrudTest extends TestCase
->set('projectForm.team_id', $fixtures['team']->id)
->set('projectForm.title', 'Customer Portal')
->set('projectForm.slug', 'customer-portal')
->set('projectForm.color', 'cyan')
->set('projectForm.color', 'blue')
->call('saveProject')
->assertHasNoErrors();
$project = Project::query()->where('slug', 'customer-portal')->firstOrFail();
@@ -223,7 +223,7 @@ final class SettingsCrudTest extends TestCase
Livewire::test(SettingsManager::class)
->set('tagForm.name', 'VIP')
->set('tagForm.slug', 'vip')
->set('tagForm.color', 'pink')
->set('tagForm.color', 'purple')
->call('saveTag')
->assertHasNoErrors();
$tag = Tag::query()->where('slug', 'vip')->firstOrFail();
@@ -248,4 +248,22 @@ final class SettingsCrudTest extends TestCase
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_products', ['id' => $product->id, 'version' => '1.1']);
}
public function test_helpdesk_colour_selectors_are_dropdowns_and_reject_unknown_values(): void
{
$this->seedLookupData();
$admin = $this->admin(['name' => 'Colour Settings Admin']);
$this->actingAs($admin);
Livewire::test(SettingsManager::class)
->assertSee('● Red — #dc2626')
->assertSee('● Green — #16a34a')
->set('statusForm.name', 'Invalid Colour')
->set('statusForm.slug', 'invalid-colour')
->set('statusForm.color', 'brown')
->set('statusForm.sort_order', 99)
->call('saveStatus')
->assertHasErrors(['statusForm.color']);
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Unit;
use Modules\Helpdesk\Support\Color;
use PHPUnit\Framework\TestCase;
final class ColorPaletteTest extends TestCase
{
public function test_standard_palette_uses_clear_enterprise_colours(): void
{
self::assertSame('#dc2626', Color::css('red'));
self::assertSame('#16a34a', Color::css('green'));
self::assertSame('#2563eb', Color::css('blue'));
self::assertSame('#6b7280', Color::css('gray'));
}
public function test_legacy_colour_names_are_normalised(): void
{
self::assertSame('red', Color::normalise('rose'));
self::assertSame('green', Color::normalise('emerald'));
self::assertSame('blue', Color::normalise('cyan'));
self::assertSame('gray', Color::normalise('zinc'));
}
public function test_hex_values_can_still_be_resolved_for_display(): void
{
self::assertSame('#ffffff', Color::css('#ffffff'));
self::assertSame('#000000', Color::css('#000000'));
}
}
+13
View File
@@ -191,3 +191,16 @@
- Documented that fake inline spellcheck must not replace the rich editor.
- Refreshed ticket time aggregates and the time-log modal after timer start and stop without requiring a page reload.
- Added regression coverage for timer refresh and spellcheck/editor boundary behaviour.
- version: 1.9.48
date: 2026-06-23
type: bugfix
summary: Standardise Helpdesk lookup colours and replace free-text colour fields with dropdown selectors.
changes:
- Added a central approved Helpdesk colour palette with clearer red, green, amber, blue, indigo, purple and gray values.
- Normalised legacy Flux and Tailwind colour aliases such as rose, emerald, sky, cyan, violet and zinc to approved WMS colour keys.
- Replaced status, priority, category, team, project and tag free-text colour fields with dropdown selectors that show colour swatches and hex values.
- Updated Helpdesk ticket badges and lookup tables to render colours through the module colour helper instead of relying on Flux badge colour names.
- Added a migration to clean existing database lookup colour values to the approved palette.
- Updated Helpdesk seed data, feature coverage and unit coverage for the standardised colour behaviour.
- Preserved append-only changelog history.
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.9.47"
"version": "1.9.48"
}