From 5dbc6394b8ec0010c16b12809cd4db1b7b5c18e0 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 23 Jun 2026 15:37:01 +0100 Subject: [PATCH] standard colours with noce UI and lasyout changes --- .../Seeders/HelpdeskDatabaseSeeder.php | 20 +++-- Modules/Helpdesk/Livewire/SettingsManager.php | 12 +-- .../views/components/colour-badge.blade.php | 12 ++- .../views/components/colour-select.blade.php | 79 ++++++++++++++----- Modules/Helpdesk/Support/Color.php | 71 +++++++++++++++-- .../Helpdesk/Tests/Unit/ColorPaletteTest.php | 22 +++++- Modules/Helpdesk/changelog.yaml | 14 ++++ Modules/Helpdesk/module.json | 2 +- 8 files changed, 184 insertions(+), 48 deletions(-) diff --git a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php index 25ebe9d5..3e9f7d1b 100644 --- a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php +++ b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php @@ -46,11 +46,13 @@ final class HelpdeskDatabaseSeeder extends Seeder foreach ([ ['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' => '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], + ['name' => 'Open', 'slug' => 'open', 'color' => 'green', 'sort_order' => 10, 'is_closed' => false, 'is_default' => false], + ['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'indigo', 'sort_order' => 20, 'is_closed' => false, 'is_default' => false], + ['name' => 'Waiting Customer', 'slug' => 'waiting-customer', 'color' => 'amber', 'sort_order' => 30, 'is_closed' => false, 'is_default' => false], + ['name' => 'Waiting Third Party', 'slug' => 'waiting-third-party', 'color' => 'purple', 'sort_order' => 40, 'is_closed' => false, 'is_default' => false], + ['name' => 'Resolved', 'slug' => 'resolved', 'color' => 'green', 'sort_order' => 80, 'is_closed' => true, 'is_default' => false], + ['name' => 'Closed', 'slug' => 'closed', 'color' => 'gray', 'sort_order' => 90, 'is_closed' => true, 'is_default' => false], + ['name' => 'Cancelled', 'slug' => 'cancelled', 'color' => 'red', 'sort_order' => 100, 'is_closed' => true, 'is_default' => false], ] as $row) { Status::query()->updateOrCreate(['slug' => $row['slug']], $row); } @@ -64,9 +66,11 @@ final class HelpdeskDatabaseSeeder extends Seeder Priority::query()->where('is_default', true)->update(['is_default' => false]); 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' => 'green', 'sort_order' => 30, 'is_default' => false], + ['name' => 'Low', 'slug' => 'low', 'color' => 'gray', 'sort_order' => 10, 'is_default' => false], + ['name' => 'Normal', 'slug' => 'normal', 'color' => 'blue', 'sort_order' => 20, 'is_default' => true], + ['name' => 'High', 'slug' => 'high', 'color' => 'amber', 'sort_order' => 30, 'is_default' => false], + ['name' => 'Urgent', 'slug' => 'urgent', 'color' => 'red', 'sort_order' => 40, 'is_default' => false], + ['name' => 'Critical', 'slug' => 'critical', 'color' => 'red', 'sort_order' => 50, 'is_default' => false], ] as $row) { Priority::query()->updateOrCreate(['slug' => $row['slug']], $row); } diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php index 34213ba8..155b2968 100644 --- a/Modules/Helpdesk/Livewire/SettingsManager.php +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -129,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', Rule::in(Color::keys())], + 'statusForm.color' => ['required', 'string', Color::validationRule()], 'statusForm.sort_order' => ['required', 'integer', 'min:0'], 'statusForm.is_closed' => ['boolean'], 'statusForm.is_default' => ['boolean'], @@ -170,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', Rule::in(Color::keys())], + 'priorityForm.color' => ['required', 'string', Color::validationRule()], 'priorityForm.sort_order' => ['required', 'integer', 'min:0'], 'priorityForm.is_default' => ['boolean'], ])['priorityForm']; @@ -212,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', Rule::in(Color::keys())], + 'categoryForm.color' => ['required', 'string', Color::validationRule()], 'categoryForm.description' => ['nullable', 'string'], 'categoryForm.is_default' => ['boolean'], ])['categoryForm']; @@ -254,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', Rule::in(Color::keys())], + 'teamForm.color' => ['required', 'string', Color::validationRule()], 'teamForm.description' => ['nullable', 'string'], 'teamForm.is_default' => ['boolean'], ])['teamForm']; @@ -297,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', Rule::in(Color::keys())], + 'projectForm.color' => ['required', 'string', Color::validationRule()], 'projectForm.description' => ['nullable', 'string'], 'projectForm.is_default' => ['boolean'], ])['projectForm']; @@ -644,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', Rule::in(Color::keys())], + 'tagForm.color' => ['required', 'string', Color::validationRule()], ])['tagForm']; $data['slug'] = $this->slug($data['slug'] ?: $data['name']); $this->persistSettingModel(Tag::class, $this->tagForm['id'], $data); diff --git a/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php b/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php index 78c488b6..c65e0651 100644 --- a/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php +++ b/Modules/Helpdesk/Resources/views/components/colour-badge.blade.php @@ -1,10 +1,16 @@ @props([ 'color' => 'gray', + 'label' => null, ]) +@php + $background = \Modules\Helpdesk\Support\Color::css($color); + $foreground = \Modules\Helpdesk\Support\Color::text($color); +@endphp + 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) }};" + {{ $attributes->class('inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold shadow-sm ring-1 ring-zinc-900/10') }} + style="background-color: {{ $background }}; color: {{ $foreground }};" > - {{ $slot }} + {{ $label ?? $slot }} diff --git a/Modules/Helpdesk/Resources/views/components/colour-select.blade.php b/Modules/Helpdesk/Resources/views/components/colour-select.blade.php index cc3e9c6a..fbdd3ded 100644 --- a/Modules/Helpdesk/Resources/views/components/colour-select.blade.php +++ b/Modules/Helpdesk/Resources/views/components/colour-select.blade.php @@ -4,31 +4,68 @@ 'options' => [], ]) -
class('space-y-2') }}> +@php + $currentValue = data_get($this, $model, 'gray'); + $currentHex = \Modules\Helpdesk\Support\Color::css($currentValue); + $currentText = \Modules\Helpdesk\Support\Color::text($currentValue); + $currentLabel = \Modules\Helpdesk\Support\Color::label($currentValue); +@endphp + +
class('space-y-3') }}> - - -
- @foreach ($options as $key => $option) - +
+ + +
+ @foreach ($options as $key => $option) + + @endforeach +
+
+ + + {{ $currentLabel }} + +
+ +
+ + +
diff --git a/Modules/Helpdesk/Support/Color.php b/Modules/Helpdesk/Support/Color.php index 2e714f4b..fc8e1ee4 100644 --- a/Modules/Helpdesk/Support/Color.php +++ b/Modules/Helpdesk/Support/Color.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Modules\Helpdesk\Support; +use Closure; + final class Color { /** @@ -41,23 +43,37 @@ final class Color ]; /** - * Resolve a stored colour key or supported hex value into a CSS colour. + * Resolve a stored palette key or supported custom hex value into a CSS colour. */ public static function css(?string $color): string { $resolved = self::normalise($color); - if (str_starts_with($resolved, '#')) { + if (self::isHex($resolved)) { return $resolved; } return self::PALETTE[$resolved] ?? self::PALETTE['gray']; } + /** + * Return a readable foreground colour for a badge background. + */ + public static function text(?string $color): string + { + $hex = ltrim(self::css($color), '#'); + $red = hexdec(substr($hex, 0, 2)); + $green = hexdec(substr($hex, 2, 2)); + $blue = hexdec(substr($hex, 4, 2)); + $luminance = (($red * 299) + ($green * 587) + ($blue * 114)) / 1000; + + return $luminance >= 150 ? '#111827' : '#ffffff'; + } + /** * Return approved palette options for settings dropdowns. * - * @return array + * @return array */ public static function options(): array { @@ -66,6 +82,7 @@ final class Color $name => [ 'name' => str($name)->headline()->toString(), 'hex' => $hex, + 'foreground' => self::text($hex), ], ]) ->all(); @@ -82,16 +99,60 @@ final class Color } /** - * Normalise legacy aliases and supported hex colours. + * Normalise legacy aliases and supported custom hex colours. */ public static function normalise(?string $color): string { $value = strtolower(trim((string) $color)); - if (preg_match('/^#[a-f0-9]{6}$/', $value) === 1) { + if (self::isHex($value)) { return $value; } return self::ALIASES[$value] ?? $value; } + + /** + * Determine whether the value is a supported six-character hex colour. + */ + public static function isHex(?string $color): bool + { + return preg_match('/^#[a-f0-9]{6}$/', strtolower(trim((string) $color))) === 1; + } + + /** + * Determine whether a stored colour is an approved palette key or custom hex colour. + */ + public static function isAllowed(?string $color): bool + { + $resolved = self::normalise($color); + + return self::isHex($resolved) || in_array($resolved, self::keys(), true); + } + + /** + * Laravel validation rule allowing approved palette keys and six-character hex values. + */ + public static function validationRule(): Closure + { + return static function (string $attribute, mixed $value, Closure $fail): void { + if (! self::isAllowed(is_string($value) ? $value : null)) { + $fail('The '.$attribute.' must be an approved Helpdesk colour or a valid hex colour such as #ffffff.'); + } + }; + } + + /** + * Human-readable label for palette and custom hex values. + */ + public static function label(?string $color): string + { + $resolved = self::normalise($color); + + if (self::isHex($resolved)) { + return strtoupper($resolved); + } + + return str($resolved ?: 'gray')->headline()->toString(); + } } diff --git a/Modules/Helpdesk/Tests/Unit/ColorPaletteTest.php b/Modules/Helpdesk/Tests/Unit/ColorPaletteTest.php index bba92458..6ecda70b 100644 --- a/Modules/Helpdesk/Tests/Unit/ColorPaletteTest.php +++ b/Modules/Helpdesk/Tests/Unit/ColorPaletteTest.php @@ -5,11 +5,12 @@ declare(strict_types=1); namespace Modules\Helpdesk\Tests\Unit; use Modules\Helpdesk\Support\Color; -use PHPUnit\Framework\TestCase; +use Tests\TestCase; final class ColorPaletteTest extends TestCase { - public function test_standard_palette_uses_clear_enterprise_colours(): void + /** @test */ + public function palette_colours_resolve_to_standard_hex_values(): void { self::assertSame('#dc2626', Color::css('red')); self::assertSame('#16a34a', Color::css('green')); @@ -17,7 +18,8 @@ final class ColorPaletteTest extends TestCase self::assertSame('#6b7280', Color::css('gray')); } - public function test_legacy_colour_names_are_normalised(): void + /** @test */ + public function legacy_colour_aliases_are_normalised(): void { self::assertSame('red', Color::normalise('rose')); self::assertSame('green', Color::normalise('emerald')); @@ -25,9 +27,21 @@ final class ColorPaletteTest extends TestCase self::assertSame('gray', Color::normalise('zinc')); } - public function test_hex_values_can_still_be_resolved_for_display(): void + /** @test */ + public function custom_hex_colours_are_supported_and_validated(): void { self::assertSame('#ffffff', Color::css('#ffffff')); self::assertSame('#000000', Color::css('#000000')); + self::assertTrue(Color::isAllowed('#ffffff')); + self::assertFalse(Color::isAllowed('#fff')); + self::assertFalse(Color::isAllowed('brown')); + } + + /** @test */ + public function readable_text_colour_is_calculated_for_badge_backgrounds(): void + { + self::assertSame('#111827', Color::text('#ffffff')); + self::assertSame('#ffffff', Color::text('#000000')); + self::assertSame('#ffffff', Color::text('red')); } } diff --git a/Modules/Helpdesk/changelog.yaml b/Modules/Helpdesk/changelog.yaml index e351cc1c..cd78f896 100644 --- a/Modules/Helpdesk/changelog.yaml +++ b/Modules/Helpdesk/changelog.yaml @@ -204,3 +204,17 @@ - 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. + +- version: 1.9.49 + date: 2026-06-23 + type: feature + summary: Add Helpdesk custom hex colours, contrast-safe badges and standardised seeded workflows. + changes: + - Added support for six-character custom hex colours such as #ffffff while keeping the approved WMS palette as the default path. + - Added colour picker, custom hex input, clickable palette swatches and live preview badge behaviour to Helpdesk colour selectors. + - Added contrast-safe badge text calculation for light and dark colour backgrounds. + - Added a reusable Helpdesk badge component so lists, Kanban cards, ticket detail pages, reports and notifications can render colours consistently. + - Standardised seeded ticket statuses for New, Open, In Progress, Waiting Customer, Waiting Third Party, Resolved, Closed and Cancelled. + - Standardised seeded ticket priorities for Low, Normal, High, Urgent and Critical. + - Extended unit and file-level regression coverage for custom hex colours, colour selector UI and contrast-safe badge rendering. + - Preserved append-only changelog history. diff --git a/Modules/Helpdesk/module.json b/Modules/Helpdesk/module.json index 5248cc1a..116cc2d2 100644 --- a/Modules/Helpdesk/module.json +++ b/Modules/Helpdesk/module.json @@ -22,5 +22,5 @@ "Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider" ], "files": [], - "version": "1.9.48" + "version": "1.9.49" }