Repo rebuild

This commit is contained in:
Mark Gregory
2026-09-15 12:33:28 +01:00
commit be28c4feec
5629 changed files with 317074 additions and 0 deletions
@@ -0,0 +1,16 @@
@props([
'color' => 'gray',
'label' => null,
])
@php
$background = \Modules\Helpdesk\Support\Color::css($color);
$foreground = \Modules\Helpdesk\Support\Color::text($color);
@endphp
<span
{{ $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 }};"
>
{{ $label ?? $slot }}
</span>
@@ -0,0 +1,74 @@
@props([
'label' => 'Badge colour',
'model',
'options' => [],
])
@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
<div {{ $attributes->class('rounded-xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-950/50') }}>
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div class="min-w-0 space-y-2">
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-600 dark:text-zinc-300">
{{ $label }}
</label>
<div class="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<select
wire:model.live="{{ $model }}"
class="h-9 w-full rounded-lg 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-900 dark:text-zinc-100"
>
@foreach ($options as $key => $option)
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
● {{ $option['name'] }} — {{ $option['hex'] }}
</option>
@endforeach
</select>
<span
class="inline-flex h-9 min-w-28 items-center justify-center rounded-lg px-3 text-xs font-bold shadow-sm ring-1 ring-zinc-900/10 dark:ring-white/10"
style="background-color: {{ $currentHex }}; color: {{ $currentText }};"
>
{{ $currentLabel }}
</span>
</div>
<div class="flex flex-wrap items-center gap-1.5" aria-label="Available Helpdesk colours">
@foreach ($options as $key => $option)
<button
type="button"
wire:click="$set('{{ $model }}', '{{ $key }}')"
title="{{ $option['name'] }} {{ $option['hex'] }}"
class="inline-flex h-6 w-6 items-center justify-center rounded-full border border-white/80 shadow-sm ring-1 ring-zinc-900/10 transition hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500/40 dark:ring-white/15"
style="background-color: {{ $option['hex'] }}"
>
<span class="sr-only">Use {{ $option['name'] }}</span>
</button>
@endforeach
</div>
</div>
<div class="grid grid-cols-[2.75rem_minmax(0,11rem)] items-center gap-2 lg:justify-end">
<input
type="color"
value="{{ $currentHex }}"
wire:change="$set('{{ $model }}', $event.target.value)"
class="h-9 w-11 cursor-pointer rounded-lg border border-zinc-300 bg-white p-1 shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
aria-label="Choose a custom hex colour"
/>
<input
type="text"
wire:model.live.debounce.500ms="{{ $model }}"
class="h-9 w-full rounded-lg border border-zinc-300 bg-white px-3 text-xs font-semibold text-zinc-900 placeholder:text-zinc-400 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-900 dark:text-zinc-100 dark:placeholder:text-zinc-500"
placeholder="#ffffff"
aria-label="Custom hex colour"
/>
</div>
</div>
</div>
@@ -0,0 +1,21 @@
@props(['id', 'model', 'label' => null, 'required' => false, 'placeholder' => 'Select date'])
<div>
@if ($label)
<label class="block text-sm font-semibold text-zinc-200" for="{{ $id }}">
{{ $label }}
@if ($required)
<span class="helpdesk-required-star">*</span>
@endif
</label>
@endif
<div class="mt-2" wire:ignore>
<input id="{{ $id }}" type="text" value="{{ $attributes->get('value') }}" data-helpdesk-flatpickr
data-wire-model="{{ $model }}" placeholder="{{ $placeholder }}" autocomplete="off"
{{ $attributes->except('value')->merge([
'class' =>
'block w-full rounded-xl border border-zinc-700 bg-zinc-950 px-4 py-3 text-sm text-zinc-100 shadow-sm outline-none transition-colors duration-150 placeholder:text-zinc-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/40',
]) }} />
</div>
</div>
@@ -0,0 +1,143 @@
@props([
'model' => 'body',
'label' => 'Details',
'placeholder' => 'Describe the ticket, include steps to reproduce, expected result, actual result and any useful technical details.',
'rows' => 10,
'editorDusk' => null,
])
<div
class="space-y-2"
x-data="{
model: @js($model),
value: @entangle($model),
init() {
this.$nextTick(() => {
this.writeEditorValue(this.value || '');
});
},
writeEditorValue(value) {
if (!this.$refs.editor) {
return;
}
const nextValue = value || '';
const currentValue = this.$refs.editor.innerText || '';
if (document.activeElement === this.$refs.editor && currentValue !== '' && currentValue !== nextValue) {
return;
}
this.$refs.editor.textContent = nextValue;
},
syncToLivewire(live = true) {
this.value = this.$refs.editor?.innerText || '';
if (this.$refs.input) {
this.$refs.input.value = this.value;
}
this.$wire.set(this.model, this.value, live);
},
resetEditor() {
this.value = '';
this.writeEditorValue('');
this.syncToLivewire(true);
},
runCommand(command) {
this.$refs.editor?.focus();
document.execCommand(command, false, null);
this.syncToLivewire(true);
},
wrapBlock(tagName) {
this.$refs.editor?.focus();
document.execCommand('formatBlock', false, tagName);
this.syncToLivewire(true);
},
}"
x-on:helpdesk-editor-sync.window="if (! $event.detail?.model || $event.detail.model === @js($model)) syncToLivewire($event.detail?.live ?? true)"
x-on:helpdesk-editor-reset.window="if (! $event.detail?.model || $event.detail.model === @js($model)) resetEditor()"
>
<label for="{{ $model }}-editor" class="block text-sm font-semibold text-zinc-200">
{{ $label }}
</label>
<div class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-950 shadow-sm">
<div class="flex flex-wrap items-center gap-1.5 border-b border-zinc-700 bg-zinc-900/80 px-4 py-2 text-sm text-zinc-300" data-helpdesk-editor-toolbar>
<span class="mr-2 rounded-lg bg-zinc-800 px-2 py-1 text-xs font-black uppercase tracking-wide text-zinc-100">Spellcheck enabled</span>
<button type="button" x-on:click="runCommand('bold')" data-helpdesk-editor-command="bold"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70"
aria-label="Bold selected text">
B
</button>
<button type="button" x-on:click="runCommand('italic')" data-helpdesk-editor-command="italic"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black italic text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70"
aria-label="Italic selected text">
I
</button>
<button type="button" x-on:click="runCommand('underline')" data-helpdesk-editor-command="underline"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black underline text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70"
aria-label="Underline selected text">
U
</button>
<button type="button" x-on:click="runCommand('insertUnorderedList')" data-helpdesk-editor-command="insertUnorderedList"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
• List
</button>
<button type="button" x-on:click="runCommand('insertOrderedList')" data-helpdesk-editor-command="insertOrderedList"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
1. List
</button>
<button type="button" x-on:click="wrapBlock('blockquote')" data-helpdesk-editor-command="blockquote"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
Quote
</button>
<button type="button" x-on:click="wrapBlock('pre')" data-helpdesk-editor-command="pre"
class="inline-flex min-h-8 items-center justify-center rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-1.5 text-xs font-black text-zinc-100 shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-400/70">
Code
</button>
</div>
<div
id="{{ $model }}-editor"
x-ref="editor"
contenteditable="true"
spellcheck="true"
lang="en-GB"
autocorrect="on"
autocomplete="on"
autocapitalize="sentences"
inputmode="text"
role="textbox"
aria-multiline="true"
data-placeholder="{{ $placeholder }}"
data-wire-model="{{ $model }}"
data-helpdesk-sync-target="{{ $model }}"
data-helpdesk-editor-model="{{ $model }}"
@if ($editorDusk) data-dusk="{{ $editorDusk }}" dusk="{{ $editorDusk }}" @endif
x-on:paste="syncToLivewire(false)"
x-on:blur="syncToLivewire(true)"
class="helpdesk-rich-editor block min-h-[14rem] w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-zinc-950 px-5 py-4 text-base leading-7 text-zinc-100 outline-none transition placeholder:text-zinc-500 empty:before:pointer-events-none empty:before:text-zinc-500 empty:before:content-[attr(data-placeholder)] focus:ring-2 focus:ring-blue-500"
></div>
<textarea
x-ref="input"
wire:model.defer="{{ $model }}"
spellcheck="true"
lang="en-GB"
autocorrect="on"
autocomplete="on"
autocapitalize="sentences"
inputmode="text"
class="sr-only"
tabindex="-1"
aria-hidden="true"
data-helpdesk-spellcheck-mirror="{{ $model }}"
></textarea>
</div>
<p class="text-xs font-semibold text-zinc-400">
Browser spellcheck stays active in the native editable area. Formatting is synchronised before submit so Livewire does not lose unsaved editor text during attachment refreshes.
</p>
</div>
@@ -0,0 +1,9 @@
@props(['title' => null])
{{--
Helpdesk compatibility component.
Do not call <x-layouts.app> from here: when Laravel resolves this component path first,
that creates an infinite component loop. Delegate directly to the central WMS layout instead.
--}}
@include('layouts.app.sidebar')
@@ -0,0 +1,47 @@
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-3 py-2.5">Name</th>
<th class="px-3 py-2.5">Slug</th>
<th class="px-3 py-2.5">Colour</th>
<th class="px-3 py-2.5">Default</th>
<th class="px-3 py-2.5">Tickets</th>
<th class="px-3 py-2.5 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($items as $item)
<tr>
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-3 py-2.5 font-mono text-xs text-zinc-500">{{ $item->slug }}</td>
<td class="px-3 py-2.5">
<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-3 py-2.5">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}
</flux:badge>
</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-3 py-2.5 text-right">
<div class="flex justify-end gap-1.5">
<button type="button" wire:click="{{ $edit }}({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button>
<button type="button" wire:click="{{ $delete }}({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-3 py-8 text-center text-zinc-500">No records yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@@ -0,0 +1,47 @@
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-3 py-2.5">Project</th>
<th class="px-3 py-2.5">Company</th>
<th class="px-3 py-2.5">Team</th>
<th class="px-3 py-2.5">Colour</th>
<th class="px-3 py-2.5">Default</th>
<th class="px-3 py-2.5">Tickets</th>
<th class="px-3 py-2.5 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($items as $item)
<tr>
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->title }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->team?->name ?? 'No team' }}</td>
<td class="px-3 py-2.5">
<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-3 py-2.5">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-3 py-2.5 text-right">
<div class="flex justify-end gap-1.5"><button type="button"
wire:click="editProject({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteProject({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-3 py-8 text-center text-zinc-500">No projects yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@@ -0,0 +1,36 @@
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-3 py-2.5">Name</th>
<th class="px-3 py-2.5">Provider</th>
<th class="px-3 py-2.5">Project</th>
<th class="px-3 py-2.5">Commits</th>
<th class="px-3 py-2.5 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($items as $item)
<tr>
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ strtoupper($item->provider) }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->project?->title ?? 'No project' }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->commits_count ?? 0 }}</td>
<td class="px-3 py-2.5 text-right">
<div class="flex justify-end gap-1.5"><button type="button"
wire:click="editRepository({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteRepository({{ $item->id }})"
wire:confirm="Delete this repository?"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">No repositories yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@@ -0,0 +1,33 @@
@props([
'title' => 'Settings',
'description' => '',
])
<section
{{ $attributes->merge([
'class' => 'overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm ring-1 ring-zinc-900/5 dark:border-zinc-700 dark:bg-zinc-900 dark:ring-white/10',
]) }}>
<div class="border-b border-zinc-200 bg-gradient-to-r from-slate-950 via-blue-950 to-slate-900 px-4 py-3 text-white dark:border-zinc-700">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<p class="text-[10px] font-black uppercase tracking-[0.22em] text-blue-200">Configuration</p>
<h2 class="mt-0.5 text-base font-bold tracking-tight text-white">{{ $title }}</h2>
<p class="mt-1 max-w-3xl text-xs font-medium leading-5 text-blue-100/90 sm:text-sm">{{ $description }}</p>
</div>
<span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-white/10 text-blue-100 shadow-inner">
<i class="fa-solid fa-sliders text-xs" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="border-b border-zinc-200 bg-zinc-50 px-3 py-3 dark:border-zinc-800 dark:bg-zinc-950/45 sm:px-4">
<div class="mx-auto max-w-7xl rounded-xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-900/90 sm:p-4 [&_input:not([type='checkbox']):not([type='radio']):not([type='color'])]:min-h-9 [&_textarea]:min-h-20 [&_textarea]:leading-6">
{{ $form }}
</div>
</div>
<div class="bg-white dark:bg-zinc-900">
{{ $slot }}
</div>
</section>
@@ -0,0 +1,45 @@
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
<thead
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
<tr>
<th class="px-3 py-2.5">Team</th>
<th class="px-3 py-2.5">Company</th>
<th class="px-3 py-2.5">Colour</th>
<th class="px-3 py-2.5">Default</th>
<th class="px-3 py-2.5">Tickets</th>
<th class="px-3 py-2.5 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($items as $item)
<tr>
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
<td class="px-3 py-2.5">
<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-3 py-2.5">
<flux:badge color="{{ $item->is_default ?? false ? 'emerald' : 'zinc' }}">
{{ $item->is_default ?? false ? 'Default' : 'No' }}</flux:badge>
</td>
<td class="px-3 py-2.5 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-3 py-2.5 text-right">
<div class="flex justify-end gap-1.5"><button type="button"
wire:click="editTeam({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-blue">Edit</button><button type="button"
wire:click="deleteTeam({{ $item->id }})"
class="hd-btn hd-btn-sm hd-btn-red">Delete</button></div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-3 py-8 text-center text-zinc-500">No teams yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>