updated design
This commit is contained in:
@@ -17,9 +17,9 @@ final class HelpdeskDashboardWidget extends Component
|
|||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$base = Ticket::query();
|
$base = Ticket::query();
|
||||||
|
|
||||||
if ($user->isHelpdeskAdmin()) {
|
if ($user->helpdesk_admin ?? false) {
|
||||||
// no filter
|
// no filter
|
||||||
} elseif ($user->isHelpdeskAgent()) {
|
} elseif ($user->helpdesk_agent ?? false) {
|
||||||
$base->where(function ($q) use ($user) {
|
$base->where(function ($q) use ($user) {
|
||||||
$q->where('agent_id', $user->id)
|
$q->where('agent_id', $user->id)
|
||||||
->orWhere('user_id', $user->id);
|
->orWhere('user_id', $user->id);
|
||||||
@@ -34,7 +34,7 @@ final class HelpdeskDashboardWidget extends Component
|
|||||||
$closedPercent = $totalCount > 0 ? (int) round(($closedCount / $totalCount) * 100) : 0;
|
$closedPercent = $totalCount > 0 ? (int) round(($closedCount / $totalCount) * 100) : 0;
|
||||||
$openPercent = $totalCount > 0 ? (int) round(($openCount / $totalCount) * 100) : 0;
|
$openPercent = $totalCount > 0 ? (int) round(($openCount / $totalCount) * 100) : 0;
|
||||||
|
|
||||||
return view('helpdesk::livewire.dashboard-widget', [
|
return view('modules.helpdesk.livewire.dashboard-widget', [
|
||||||
'totalCount' => $totalCount,
|
'totalCount' => $totalCount,
|
||||||
'openCount' => $openCount,
|
'openCount' => $openCount,
|
||||||
'closedCount' => $closedCount,
|
'closedCount' => $closedCount,
|
||||||
|
|||||||
@@ -185,13 +185,13 @@ final class KanbanBoard extends Component
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('helpdesk::livewire.kanban-board', [
|
return view('modules.helpdesk.livewire.kanban-board', [
|
||||||
'boards' => $boards,
|
'boards' => $boards,
|
||||||
'board' => $board,
|
'board' => $board,
|
||||||
'statuses' => $board?->statuses ?? collect(),
|
'statuses' => $board?->statuses ?? collect(),
|
||||||
'ticketsByStatus' => $this->ticketsByStatus($board),
|
'ticketsByStatus' => $this->ticketsByStatus($board),
|
||||||
'refreshNonce' => $this->refreshNonce,
|
'refreshNonce' => $this->refreshNonce,
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Helpdesk Kanban')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -699,7 +699,7 @@ final class SettingsManager extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.settings-manager', [
|
return view('modules.helpdesk.livewire.settings-manager', [
|
||||||
'statuses' => $this->orderedStatuses(),
|
'statuses' => $this->orderedStatuses(),
|
||||||
'priorities' => $this->orderedPriorities(),
|
'priorities' => $this->orderedPriorities(),
|
||||||
'categories' => $this->orderedCategories(),
|
'categories' => $this->orderedCategories(),
|
||||||
@@ -718,7 +718,7 @@ final class SettingsManager extends Component
|
|||||||
'tags' => Tag::query()->orderBy('name')->get(),
|
'tags' => Tag::query()->orderBy('name')->get(),
|
||||||
'checklistTemplates' => ChecklistTemplate::query()->orderBy('name')->get(),
|
'checklistTemplates' => ChecklistTemplate::query()->orderBy('name')->get(),
|
||||||
'colourOptions' => Color::options(),
|
'colourOptions' => Color::options(),
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Helpdesk Settings')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function orderedStatuses(): mixed
|
private function orderedStatuses(): mixed
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ final class TicketCreate extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-create', [
|
return view('modules.helpdesk.livewire.ticket-create', [
|
||||||
'boards' => $this->boardOptions(),
|
'boards' => $this->boardOptions(),
|
||||||
'priorities' => $this->priorityOptions(),
|
'priorities' => $this->priorityOptions(),
|
||||||
'categories' => $this->categoryOptions(),
|
'categories' => $this->categoryOptions(),
|
||||||
@@ -146,7 +146,7 @@ final class TicketCreate extends Component
|
|||||||
'projects' => $this->projectOptions(),
|
'projects' => $this->projectOptions(),
|
||||||
'products' => Product::query()->where('is_active', true)->orderBy('name')->get(),
|
'products' => Product::query()->where('is_active', true)->orderBy('name')->get(),
|
||||||
'tags' => Tag::query()->orderBy('name')->get(),
|
'tags' => Tag::query()->orderBy('name')->get(),
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Create Helpdesk Ticket')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ final class TicketEdit extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-edit', [
|
return view('modules.helpdesk.livewire.ticket-edit', [
|
||||||
'boards' => KanbanBoard::query()->with('owner:id,name,email')->orderByDesc('is_default')->orderBy('sort_order')->orderBy('name')->get(),
|
'boards' => KanbanBoard::query()->with('owner:id,name,email')->orderByDesc('is_default')->orderBy('sort_order')->orderBy('name')->get(),
|
||||||
'statuses' => Status::query()->orderBy('sort_order')->orderBy('name')->get(),
|
'statuses' => Status::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||||
'priorities' => Priority::query()->orderBy('sort_order')->orderBy('name')->get(),
|
'priorities' => Priority::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||||
@@ -198,7 +198,7 @@ final class TicketEdit extends Component
|
|||||||
'products' => Product::query()->where('is_active', true)->orderBy('name')->get(),
|
'products' => Product::query()->where('is_active', true)->orderBy('name')->get(),
|
||||||
'tags' => Tag::query()->orderBy('name')->get(),
|
'tags' => Tag::query()->orderBy('name')->get(),
|
||||||
'agents' => $this->agentOptions(),
|
'agents' => $this->agentOptions(),
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Edit Helpdesk Ticket')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ final class TicketIndex extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-index', [
|
return view('modules.helpdesk.livewire.ticket-index', [
|
||||||
'tickets' => $this->tickets(),
|
'tickets' => $this->tickets(),
|
||||||
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
||||||
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Helpdesk Tickets')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resetTicketDeletionState(): void
|
private function resetTicketDeletionState(): void
|
||||||
|
|||||||
@@ -320,12 +320,12 @@ final class TicketShow extends Component
|
|||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-show', [
|
return view('modules.helpdesk.livewire.ticket-show', [
|
||||||
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
||||||
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
||||||
'agents' => $this->agentOptions(),
|
'agents' => $this->agentOptions(),
|
||||||
'cannedResponses' => CannedResponse::query()->where('is_active', true)->orderBy('sort_order')->orderBy('name')->get(),
|
'cannedResponses' => CannedResponse::query()->where('is_active', true)->orderBy('sort_order')->orderBy('name')->get(),
|
||||||
])->layout('helpdesk::layouts.app.sidebar');
|
])->layout('layouts.app', ['title' => __('Helpdesk Ticket')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normaliseNullableInt(mixed $value): ?int
|
private function normaliseNullableInt(mixed $value): ?int
|
||||||
@@ -691,7 +691,7 @@ final class TicketShow extends Component
|
|||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user !== null && $user->isHelpdeskAdmin();
|
return $user !== null && (method_exists($user, 'isAdmin') ? $user->isAdmin() : (bool) ($user->helpdesk_admin ?? false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection
|
private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class TicketTimer extends Component
|
|||||||
*/
|
*/
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-timer');
|
return view('modules.helpdesk.livewire.ticket-timer');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Tailwind scanner to discover module Blade classes.
|
* Tailwind scanner to discover module Blade classes.
|
||||||
*/
|
*/
|
||||||
@source "../../views/**/*.blade.php";
|
@source "../../views/**/*.blade.php";
|
||||||
@source "../../../../../resources/themes/admin/views/modules/helpdesk/**/*.blade.php";
|
@source "../../../../../resources/themes/backend/views/modules/helpdesk/**/*.blade.php";
|
||||||
@source "../../../Livewire/**/*.php";
|
@source "../../../Livewire/**/*.php";
|
||||||
|
|
||||||
.hd-helpdesk-surface .hd-btn,
|
.hd-helpdesk-surface .hd-btn,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { defineConfig } from 'vite';
|
|||||||
import laravel from 'laravel-vite-plugin';
|
import laravel from 'laravel-vite-plugin';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
console.log('[Helpdesk] Running Modules/Helpdesk/vite.config.js');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
@@ -14,6 +16,7 @@ export default defineConfig({
|
|||||||
laravel({
|
laravel({
|
||||||
publicDirectory: '../../public',
|
publicDirectory: '../../public',
|
||||||
buildDirectory: 'build/helpdesk',
|
buildDirectory: 'build/helpdesk',
|
||||||
|
hotFile: '../../public/hot-helpdesk',
|
||||||
|
|
||||||
input: [
|
input: [
|
||||||
'Resources/assets/css/app.css',
|
'Resources/assets/css/app.css',
|
||||||
@@ -23,6 +26,7 @@ export default defineConfig({
|
|||||||
refresh: [
|
refresh: [
|
||||||
'Resources/views/**',
|
'Resources/views/**',
|
||||||
'Livewire/**',
|
'Livewire/**',
|
||||||
|
'../../resources/themes/backend/views/modules/helpdesk/**',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-2
@@ -3,8 +3,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "npm run build:backend && npm run build:helpdesk",
|
||||||
"dev": "vite"
|
"build:backend": "vite build --config ./vite.config.js",
|
||||||
|
"build:helpdesk": "npm --prefix Modules/Helpdesk ci && npm --prefix Modules/Helpdesk run build",
|
||||||
|
"dev": "vite --config ./vite.config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@laravel/passkeys": "^0.2.0",
|
"@laravel/passkeys": "^0.2.0",
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
@import '../../../../../vendor/livewire/flux/dist/flux.css';
|
@import '../../../../../vendor/livewire/flux/dist/flux.css';
|
||||||
@import './helpdesk.css';
|
@import './helpdesk.css';
|
||||||
|
|
||||||
@source '../../views';
|
@source '../../views/**/*.blade.php';
|
||||||
@source '../../views/modules/helpdesk/**/*.blade.php';
|
@source '../../../../../Modules/Helpdesk/**/*.php';
|
||||||
@source '../../../../../Modules/Helpdesk/Livewire/**/*.php';
|
|
||||||
@source '../../../**/*.blade.php';
|
|
||||||
@source '../../../../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@source '../../../../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
@source '../../../../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
|
|
||||||
@source '../../../../../vendor/livewire/flux/stubs/**/*.blade.php';
|
@source '../../../../../vendor/livewire/flux/stubs/**/*.blade.php';
|
||||||
|
@source '../../../../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
|
||||||
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::after,
|
::after,
|
||||||
::before,
|
::before,
|
||||||
@@ -56,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-flux-label] {
|
[data-flux-label] {
|
||||||
@apply !mb-0 !leading-tight;
|
@apply !mb-0 !leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus[data-flux-control],
|
input:focus[data-flux-control],
|
||||||
@@ -64,7 +61,3 @@ textarea:focus[data-flux-control],
|
|||||||
select:focus[data-flux-control] {
|
select:focus[data-flux-control] {
|
||||||
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
|
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* \[:where(&)\]:size-4 {
|
|
||||||
@apply size-4;
|
|
||||||
} */
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@props([
|
||||||
|
'title',
|
||||||
|
'items',
|
||||||
|
'formPrefix',
|
||||||
|
'saveMethod',
|
||||||
|
'editMethod',
|
||||||
|
'deleteMethod',
|
||||||
|
'resetMethod',
|
||||||
|
'nameField' => 'name',
|
||||||
|
])
|
||||||
|
|
||||||
|
<section class="grid gap-6 xl:grid-cols-[22rem_minmax(0,1fr)]">
|
||||||
|
<form wire:submit="{{ $saveMethod }}" class="space-y-4 rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<flux:heading size="lg">{{ __($title) }}</flux:heading>
|
||||||
|
|
||||||
|
<flux:input wire:model="{{ $formPrefix }}.{{ $nameField }}" :label="__('Name')" required />
|
||||||
|
|
||||||
|
@if ($nameField !== 'slug')
|
||||||
|
<flux:input wire:model="{{ $formPrefix }}.slug" :label="__('Slug')" />
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<flux:button type="submit" variant="primary">{{ __('Save') }}</flux:button>
|
||||||
|
<flux:button type="button" wire:click="{{ $resetMethod }}">{{ __('Reset') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<div class="border-b border-zinc-200 px-5 py-4 dark:border-zinc-700">
|
||||||
|
<flux:heading size="sm">{{ __($title) }}</flux:heading>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||||
|
@forelse ($items as $item)
|
||||||
|
<div class="flex items-center justify-between gap-4 px-5 py-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate font-medium">{{ data_get($item, $nameField) }}</div>
|
||||||
|
@if (data_get($item, 'slug'))<div class="truncate text-xs text-zinc-500">{{ data_get($item, 'slug') }}</div>@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<flux:button size="sm" type="button" wire:click="{{ $editMethod }}({{ $item->id }})">{{ __('Edit') }}</flux:button>
|
||||||
|
<flux:button size="sm" variant="danger" type="button" wire:click="{{ $deleteMethod }}({{ $item->id }})" wire:confirm="{{ __('Delete this item?') }}">{{ __('Delete') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="px-5 py-10 text-center text-sm text-zinc-500">{{ __('No items yet.') }}</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@props([
|
||||||
|
'title' => null,
|
||||||
|
])
|
||||||
|
|
||||||
|
<x-layouts::app :title="$title" {{ $attributes }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</x-layouts::app>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<div class="hd-helpdesk-surface space-y-6" wire:key="helpdesk-kanban-{{ $refreshNonce }}">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Helpdesk Kanban') }}</flux:heading>
|
||||||
|
<flux:text class="mt-1">
|
||||||
|
{{ __('Move tickets through the workflow for the selected board.') }}
|
||||||
|
</flux:text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-end gap-3">
|
||||||
|
@if ($boards->isNotEmpty())
|
||||||
|
<div class="min-w-64">
|
||||||
|
<flux:select
|
||||||
|
wire:model.live="board_id"
|
||||||
|
:label="__('Board')"
|
||||||
|
>
|
||||||
|
@foreach ($boards as $boardOption)
|
||||||
|
<option value="{{ $boardOption->id }}">
|
||||||
|
{{ $boardOption->name }}
|
||||||
|
@if ($boardOption->is_default)
|
||||||
|
— {{ __('Default') }}
|
||||||
|
@endif
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
:href="route('helpdesk.tickets.index')"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Ticket list') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
variant="primary"
|
||||||
|
:href="route('helpdesk.tickets.create')"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('New ticket') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-200">
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($board === null)
|
||||||
|
<div class="rounded-2xl border border-dashed border-zinc-300 bg-white p-10 text-center dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<flux:heading size="lg">{{ __('No Kanban board configured') }}</flux:heading>
|
||||||
|
<flux:text class="mt-2">
|
||||||
|
{{ __('Create a board in Helpdesk settings before using Kanban.') }}
|
||||||
|
</flux:text>
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
class="mt-5"
|
||||||
|
variant="primary"
|
||||||
|
:href="route('helpdesk.settings')"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Open settings') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex gap-4 overflow-x-auto pb-4">
|
||||||
|
@foreach ($statuses as $status)
|
||||||
|
@php
|
||||||
|
$statusTickets = collect($ticketsByStatus[$status->id] ?? []);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="w-[22rem] min-w-[22rem] rounded-2xl border border-zinc-200 bg-zinc-100/70 p-3 dark:border-zinc-700 dark:bg-zinc-900"
|
||||||
|
wire:key="kanban-status-{{ $status->id }}-{{ $refreshNonce }}"
|
||||||
|
>
|
||||||
|
<header class="mb-3 flex items-center justify-between gap-3 px-1">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<flux:heading size="sm" class="truncate">
|
||||||
|
{{ $status->name }}
|
||||||
|
</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="inline-flex min-w-7 items-center justify-center rounded-full bg-white px-2 py-1 text-xs font-semibold text-zinc-600 shadow-sm dark:bg-zinc-800 dark:text-zinc-300">
|
||||||
|
{{ $statusTickets->count() }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@forelse ($statusTickets as $ticket)
|
||||||
|
<article
|
||||||
|
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm transition hover:shadow-md dark:border-zinc-700 dark:bg-zinc-800"
|
||||||
|
wire:key="kanban-ticket-{{ $ticket->id }}-{{ $ticket->status_id }}"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<a
|
||||||
|
href="{{ route('helpdesk.tickets.show', $ticket) }}"
|
||||||
|
wire:navigate
|
||||||
|
class="text-xs font-semibold text-blue-600 hover:underline dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{{ $ticket->reference }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ route('helpdesk.tickets.show', $ticket) }}"
|
||||||
|
wire:navigate
|
||||||
|
class="mt-1 block font-semibold leading-snug text-zinc-900 hover:underline dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
{{ $ticket->subject }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($ticket->priority)
|
||||||
|
<span class="shrink-0 rounded-full bg-zinc-100 px-2 py-1 text-[11px] font-semibold text-zinc-600 dark:bg-zinc-700 dark:text-zinc-200">
|
||||||
|
{{ $ticket->priority->name }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 space-y-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
@if ($ticket->agent)
|
||||||
|
<div>{{ __('Agent') }}: {{ $ticket->agent->name }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($ticket->category)
|
||||||
|
<div>{{ __('Category') }}: {{ $ticket->category->name }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($ticket->project)
|
||||||
|
<div>{{ __('Project') }}: {{ $ticket->project->name ?? $ticket->project->title ?? '—' }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@can('update', $ticket)
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="mb-1 block text-xs font-semibold text-zinc-500 dark:text-zinc-400">
|
||||||
|
{{ __('Move to') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
class="hd-select !min-h-0 !rounded-lg !px-3 !py-2 text-sm"
|
||||||
|
wire:change="moveTicket({{ $ticket->id }}, $event.target.value)"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
wire:target="moveTicket"
|
||||||
|
>
|
||||||
|
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||||||
|
|
||||||
|
@foreach ($statuses as $targetStatus)
|
||||||
|
@if ($targetStatus->id !== $status->id)
|
||||||
|
<option value="{{ $targetStatus->id }}">
|
||||||
|
{{ $targetStatus->name }}
|
||||||
|
</option>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed border-zinc-300 bg-white/70 px-4 py-8 text-center text-sm text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800/40 dark:text-zinc-400">
|
||||||
|
{{ __('No tickets') }}
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Helpdesk Settings') }}</flux:heading>
|
||||||
|
<flux:text>{{ __('Manage statuses, priorities, categories, teams, projects and Helpdesk configuration.') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate>{{ __('Back to Helpdesk') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$sections = [
|
||||||
|
'statuses' => 'Statuses',
|
||||||
|
'priorities' => 'Priorities',
|
||||||
|
'categories' => 'Categories',
|
||||||
|
'teams' => 'Teams',
|
||||||
|
'projects' => 'Projects',
|
||||||
|
'boards' => 'Boards',
|
||||||
|
'repositories' => 'Repositories',
|
||||||
|
'workflows' => 'Workflows',
|
||||||
|
'canned' => 'Canned responses',
|
||||||
|
'sla' => 'SLA policies',
|
||||||
|
'automation' => 'Automation',
|
||||||
|
'products' => 'Products',
|
||||||
|
'tags' => 'Tags',
|
||||||
|
'checklists' => 'Checklists',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 rounded-xl border border-zinc-200 bg-white p-2 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
@foreach ($sections as $key => $label)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="$set('section', '{{ $key }}')"
|
||||||
|
class="rounded-lg px-3 py-2 text-sm font-medium {{ $section === $key ? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900' : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800' }}"
|
||||||
|
>{{ __($label) }}</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($section === 'statuses')
|
||||||
|
<x-helpdesk-settings-simple title="Statuses" :items="$statuses" form-prefix="statusForm" save-method="saveStatus" edit-method="editStatus" delete-method="deleteStatus" reset-method="resetStatusForm" />
|
||||||
|
@elseif ($section === 'priorities')
|
||||||
|
<x-helpdesk-settings-simple title="Priorities" :items="$priorities" form-prefix="priorityForm" save-method="savePriority" edit-method="editPriority" delete-method="deletePriority" reset-method="resetPriorityForm" />
|
||||||
|
@elseif ($section === 'categories')
|
||||||
|
<x-helpdesk-settings-simple title="Categories" :items="$categories" form-prefix="categoryForm" save-method="saveCategory" edit-method="editCategory" delete-method="deleteCategory" reset-method="resetCategoryForm" />
|
||||||
|
@elseif ($section === 'teams')
|
||||||
|
<x-helpdesk-settings-simple title="Teams" :items="$teams" form-prefix="teamForm" save-method="saveTeam" edit-method="editTeam" delete-method="deleteTeam" reset-method="resetTeamForm" />
|
||||||
|
@elseif ($section === 'projects')
|
||||||
|
<x-helpdesk-settings-simple title="Projects" :items="$projects" form-prefix="projectForm" save-method="saveProject" edit-method="editProject" delete-method="deleteProject" reset-method="resetProjectForm" name-field="title" />
|
||||||
|
@elseif ($section === 'boards')
|
||||||
|
<x-helpdesk-settings-simple title="Boards" :items="$boards" form-prefix="boardForm" save-method="saveBoard" edit-method="editBoard" delete-method="deleteBoard" reset-method="resetBoardForm" />
|
||||||
|
@elseif ($section === 'repositories')
|
||||||
|
<x-helpdesk-settings-simple title="Repositories" :items="$repositories" form-prefix="repositoryForm" save-method="saveRepository" edit-method="editRepository" delete-method="deleteRepository" reset-method="resetRepositoryForm" />
|
||||||
|
@elseif ($section === 'workflows')
|
||||||
|
<x-helpdesk-settings-simple title="Workflow templates" :items="$workflowTemplates" form-prefix="workflowForm" save-method="saveWorkflowTemplate" edit-method="editWorkflowTemplate" delete-method="deleteWorkflowTemplate" reset-method="resetWorkflowTemplateForm" />
|
||||||
|
@elseif ($section === 'canned')
|
||||||
|
<x-helpdesk-settings-simple title="Canned responses" :items="$cannedResponses" form-prefix="cannedForm" save-method="saveCannedResponse" edit-method="editCannedResponse" delete-method="deleteCannedResponse" reset-method="resetCannedResponseForm" />
|
||||||
|
@elseif ($section === 'sla')
|
||||||
|
<x-helpdesk-settings-simple title="SLA policies" :items="$slaPolicies" form-prefix="slaForm" save-method="saveSlaPolicy" edit-method="editSlaPolicy" delete-method="deleteSlaPolicy" reset-method="resetSlaPolicyForm" />
|
||||||
|
@elseif ($section === 'automation')
|
||||||
|
<x-helpdesk-settings-simple title="Automation rules" :items="$automationRules" form-prefix="automationForm" save-method="saveAutomationRule" edit-method="editAutomationRule" delete-method="deleteAutomationRule" reset-method="resetAutomationRuleForm" />
|
||||||
|
@elseif ($section === 'products')
|
||||||
|
<x-helpdesk-settings-simple title="Products" :items="$products" form-prefix="productForm" save-method="saveProduct" edit-method="editProduct" delete-method="deleteProduct" reset-method="resetProductForm" />
|
||||||
|
@elseif ($section === 'tags')
|
||||||
|
<x-helpdesk-settings-simple title="Tags" :items="$tags" form-prefix="tagForm" save-method="saveTag" edit-method="editTag" delete-method="deleteTag" reset-method="resetTagForm" />
|
||||||
|
@elseif ($section === 'checklists')
|
||||||
|
<x-helpdesk-settings-simple title="Checklist templates" :items="$checklistTemplates" form-prefix="checklistTemplateForm" save-method="saveChecklistTemplate" edit-method="editChecklistTemplate" delete-method="deleteChecklistTemplate" reset-method="resetChecklistTemplateForm" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Create ticket') }}</flux:heading>
|
||||||
|
<flux:text>{{ __('Create a new Helpdesk request.') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate>{{ __('Back to tickets') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit="save" class="space-y-6">
|
||||||
|
<div class="grid gap-6 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900 lg:grid-cols-2">
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<flux:input wire:model="subject" :label="__('Subject')" required />
|
||||||
|
@error('subject') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<flux:textarea wire:model="body" rows="10" :label="__('Description')" required />
|
||||||
|
@error('body') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:select wire:model="board_id" :label="__('Board')">
|
||||||
|
<option value="">{{ __('No board') }}</option>
|
||||||
|
@foreach ($boards as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="priority_id" :label="__('Priority')" required>
|
||||||
|
<option value="">{{ __('Select priority') }}</option>
|
||||||
|
@foreach ($priorities as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="category_id" :label="__('Category')">
|
||||||
|
<option value="">{{ __('No category') }}</option>
|
||||||
|
@foreach ($categories as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="team_id" :label="__('Team')">
|
||||||
|
<option value="">{{ __('No team') }}</option>
|
||||||
|
@foreach ($teams as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="project_id" :label="__('Project')">
|
||||||
|
<option value="">{{ __('No project') }}</option>
|
||||||
|
@foreach ($projects as $item)<option value="{{ $item->id }}">{{ $item->title }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="product_id" :label="__('Product')">
|
||||||
|
<option value="">{{ __('No product') }}</option>
|
||||||
|
@foreach ($products as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:input wire:model="due_at" type="date" :label="__('Due date')" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('Tags') }}</label>
|
||||||
|
<select wire:model="tag_ids" multiple class="w-full rounded-lg border-zinc-300 bg-white dark:border-zinc-700 dark:bg-zinc-800">
|
||||||
|
@foreach ($tags as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('Attachments') }}</label>
|
||||||
|
<input wire:model="attachments" type="file" multiple class="block w-full text-sm text-zinc-600 dark:text-zinc-300">
|
||||||
|
@error('attachments.*') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<flux:button type="button" wire:click="clearForm">{{ __('Clear') }}</flux:button>
|
||||||
|
<flux:button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="save">
|
||||||
|
<span wire:loading.remove wire:target="save">{{ __('Create ticket') }}</span>
|
||||||
|
<span wire:loading wire:target="save">{{ __('Saving…') }}</span>
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Edit :reference', ['reference' => $ticket->reference]) }}</flux:heading>
|
||||||
|
<flux:text>{{ __('Update ticket details and assignment.') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
<flux:button :href="route('helpdesk.tickets.show', $ticket)" wire:navigate>{{ __('Cancel') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit="save" class="space-y-6">
|
||||||
|
<div class="grid gap-6 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900 lg:grid-cols-2">
|
||||||
|
<div class="lg:col-span-2"><flux:input wire:model="subject" :label="__('Subject')" required /></div>
|
||||||
|
<div class="lg:col-span-2"><flux:textarea wire:model="body" rows="10" :label="__('Description')" required /></div>
|
||||||
|
|
||||||
|
<flux:select wire:model="status_id" :label="__('Status')">@foreach ($statuses as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
<flux:select wire:model="priority_id" :label="__('Priority')">@foreach ($priorities as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="board_id" :label="__('Board')"><option value="">{{ __('No board') }}</option>@foreach ($boards as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
<flux:select wire:model="agent_id" :label="__('Agent')"><option value="">{{ __('Unassigned') }}</option>@foreach ($agents as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="category_id" :label="__('Category')"><option value="">{{ __('No category') }}</option>@foreach ($categories as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
<flux:select wire:model="team_id" :label="__('Team')"><option value="">{{ __('No team') }}</option>@foreach ($teams as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model="project_id" :label="__('Project')"><option value="">{{ __('No project') }}</option>@foreach ($projects as $item)<option value="{{ $item->id }}">{{ $item->title }}</option>@endforeach</flux:select>
|
||||||
|
<flux:select wire:model="product_id" :label="__('Product')"><option value="">{{ __('No product') }}</option>@foreach ($products as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach</flux:select>
|
||||||
|
|
||||||
|
<flux:input wire:model="due_at" type="date" :label="__('Due date')" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('Tags') }}</label>
|
||||||
|
<select wire:model="tag_ids" multiple class="w-full rounded-lg border-zinc-300 bg-white dark:border-zinc-700 dark:bg-zinc-800">
|
||||||
|
@foreach ($tags as $item)<option value="{{ $item->id }}">{{ $item->name }}</option>@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('New attachments') }}</label>
|
||||||
|
<input wire:model="attachments" type="file" multiple class="block w-full text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($ticket->attachments->isNotEmpty())
|
||||||
|
<div class="lg:col-span-2 space-y-2">
|
||||||
|
<flux:heading size="sm">{{ __('Existing attachments') }}</flux:heading>
|
||||||
|
@foreach ($ticket->attachments as $attachment)
|
||||||
|
<div class="flex items-center justify-between rounded-lg border border-zinc-200 px-3 py-2 dark:border-zinc-700">
|
||||||
|
<span class="text-sm">{{ $attachment->original_name ?? $attachment->filename ?? __('Attachment') }}</span>
|
||||||
|
<flux:button size="sm" variant="danger" type="button" wire:click="removeAttachment({{ $attachment->id }})">{{ __('Remove') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<flux:button type="button" wire:click="clearForm">{{ __('Clear') }}</flux:button>
|
||||||
|
<flux:button type="submit" variant="primary">{{ __('Save changes') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Tickets') }}</flux:heading>
|
||||||
|
<flux:text>{{ __('Manage Helpdesk tickets, assignments and status.') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<flux:button :href="route('helpdesk.kanban')" wire:navigate>
|
||||||
|
{{ __('Kanban') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
<flux:button variant="primary" :href="route('helpdesk.tickets.create')" wire:navigate>
|
||||||
|
{{ __('New ticket') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 md:grid-cols-4">
|
||||||
|
<flux:input
|
||||||
|
wire:model.live.debounce.300ms="search"
|
||||||
|
:label="__('Search')"
|
||||||
|
:placeholder="__('Reference, subject or text')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<flux:select wire:model.live="status" :label="__('Status')">
|
||||||
|
<option value="">{{ __('All statuses') }}</option>
|
||||||
|
@foreach ($statuses as $statusOption)
|
||||||
|
<option value="{{ $statusOption->id }}">{{ $statusOption->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model.live="priority" :label="__('Priority')">
|
||||||
|
<option value="">{{ __('All priorities') }}</option>
|
||||||
|
@foreach ($priorities as $priorityOption)
|
||||||
|
<option value="{{ $priorityOption->id }}">{{ $priorityOption->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
|
||||||
|
<flux:select wire:model.live="scope" :label="__('Scope')">
|
||||||
|
<option value="open">{{ __('Open') }}</option>
|
||||||
|
<option value="closed">{{ __('Closed') }}</option>
|
||||||
|
<option value="all">{{ __('All') }}</option>
|
||||||
|
</flux:select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||||
|
<thead class="bg-zinc-50 dark:bg-zinc-800/80">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Reference') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Subject') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Status') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Priority') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Agent') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
||||||
|
{{ __('Actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||||
|
@forelse ($tickets as $ticket)
|
||||||
|
<tr wire:key="ticket-{{ $ticket->id }}" class="hover:bg-zinc-50/70 dark:hover:bg-zinc-800/50">
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
|
<a
|
||||||
|
href="{{ route('helpdesk.tickets.show', $ticket) }}"
|
||||||
|
wire:navigate
|
||||||
|
class="hover:underline"
|
||||||
|
>
|
||||||
|
{{ $ticket->reference }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<a
|
||||||
|
href="{{ route('helpdesk.tickets.show', $ticket) }}"
|
||||||
|
wire:navigate
|
||||||
|
class="font-medium text-zinc-900 hover:underline dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
{{ $ticket->subject }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500">
|
||||||
|
@if ($ticket->category)
|
||||||
|
<span>{{ $ticket->category->name }}</span>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($ticket->project)
|
||||||
|
<span>{{ $ticket->project->title }}</span>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($ticket->attachments_count)
|
||||||
|
<span>{{ trans_choice(':count attachment|:count attachments', $ticket->attachments_count, ['count' => $ticket->attachments_count]) }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
|
{{ $ticket->status?->name ?? '—' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
|
{{ $ticket->priority?->name ?? '—' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
|
{{ $ticket->agent_name ?? '—' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<flux:button
|
||||||
|
size="sm"
|
||||||
|
:href="route('helpdesk.tickets.show', $ticket)"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('View') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
@can('update', $ticket)
|
||||||
|
<flux:button
|
||||||
|
size="sm"
|
||||||
|
:href="route('helpdesk.tickets.edit', $ticket)"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Edit') }}
|
||||||
|
</flux:button>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('delete', $ticket)
|
||||||
|
<flux:button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
wire:click="askToDeleteTicket({{ $ticket->id }})"
|
||||||
|
>
|
||||||
|
{{ __('Delete') }}
|
||||||
|
</flux:button>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-12 text-center">
|
||||||
|
<flux:heading size="lg">{{ __('No tickets found') }}</flux:heading>
|
||||||
|
<flux:text class="mt-1">
|
||||||
|
{{ __('Try changing the filters or create a new ticket.') }}
|
||||||
|
</flux:text>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($tickets->hasPages())
|
||||||
|
<div class="border-t border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
||||||
|
{{ $tickets->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($deleteModalOpen)
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||||
|
wire:key="delete-ticket-modal"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-zinc-900">
|
||||||
|
<flux:heading size="lg">{{ __('Delete ticket?') }}</flux:heading>
|
||||||
|
|
||||||
|
<flux:text class="mt-2">
|
||||||
|
{{ __('This will delete :reference — :subject.', [
|
||||||
|
'reference' => $pendingDeleteTicketReference,
|
||||||
|
'subject' => $pendingDeleteTicketSubject,
|
||||||
|
]) }}
|
||||||
|
</flux:text>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end gap-2">
|
||||||
|
<flux:button wire:click="cancelTicketDelete">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
<flux:button variant="danger" wire:click="confirmTicketDelete">
|
||||||
|
{{ __('Delete') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-zinc-500">{{ $ticket->reference }}</div>
|
||||||
|
<flux:heading size="xl">{{ $ticket->subject }}</flux:heading>
|
||||||
|
<flux:text class="mt-1">{{ __('Opened by :name', ['name' => $ticket->user?->name ?? __('Unknown')]) }}</flux:text>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate>{{ __('Tickets') }}</flux:button>
|
||||||
|
@can('update', $ticket)
|
||||||
|
<flux:button variant="primary" :href="route('helpdesk.tickets.edit', $ticket)" wire:navigate>{{ __('Edit') }}</flux:button>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<section class="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<div class="whitespace-pre-wrap text-sm leading-6 text-zinc-700 dark:text-zinc-300">{{ $ticket->body }}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<flux:heading size="lg">{{ __('Comments') }}</flux:heading>
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
@forelse ($ticket->comments as $comment)
|
||||||
|
<article class="rounded-xl bg-zinc-50 p-4 dark:bg-zinc-800">
|
||||||
|
<div class="flex justify-between gap-3 text-xs text-zinc-500">
|
||||||
|
<span>{{ $comment->user?->name ?? __('System') }}</span>
|
||||||
|
<span>{{ $comment->created_at?->diffForHumans() }}</span>
|
||||||
|
</div>
|
||||||
|
@if ($comment->subject)<div class="mt-2 font-semibold">{{ $comment->subject }}</div>@endif
|
||||||
|
<div class="mt-2 whitespace-pre-wrap text-sm">{{ $comment->body }}</div>
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<flux:text>{{ __('No comments yet.') }}</flux:text>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-3">
|
||||||
|
<flux:textarea wire:model="commentBody" rows="5" :label="__('Add comment')" />
|
||||||
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" wire:model="internal"> {{ __('Internal note') }}</label>
|
||||||
|
<flux:button wire:click="addComment" variant="primary">{{ __('Add comment') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<livewire:helpdesk.ticket-timer :ticket="$ticket" :key="'timer-'.$ticket->id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="space-y-4">
|
||||||
|
<section class="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<flux:heading size="sm">{{ __('Ticket details') }}</flux:heading>
|
||||||
|
<dl class="mt-4 space-y-3 text-sm">
|
||||||
|
<div><dt class="text-zinc-500">{{ __('Status') }}</dt><dd class="font-medium">{{ $ticket->status?->name ?? '—' }}</dd></div>
|
||||||
|
<div><dt class="text-zinc-500">{{ __('Priority') }}</dt><dd class="font-medium">{{ $ticket->priority?->name ?? '—' }}</dd></div>
|
||||||
|
<div><dt class="text-zinc-500">{{ __('Agent') }}</dt><dd class="font-medium">{{ $ticket->agent?->name ?? __('Unassigned') }}</dd></div>
|
||||||
|
<div><dt class="text-zinc-500">{{ __('Category') }}</dt><dd class="font-medium">{{ $ticket->category?->name ?? '—' }}</dd></div>
|
||||||
|
<div><dt class="text-zinc-500">{{ __('Project') }}</dt><dd class="font-medium">{{ $ticket->project?->title ?? '—' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<section class="rounded-2xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="sm">{{ __('Time tracking') }}</flux:heading>
|
||||||
|
<flux:text>{{ $this->elapsedTime }}</flux:text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($this->isRunning)
|
||||||
|
<flux:button variant="danger" wire:click="endTimer">{{ __('Stop timer') }}</flux:button>
|
||||||
|
@else
|
||||||
|
<flux:button variant="primary" wire:click="startTimer">{{ __('Start timer') }}</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<flux:input wire:model="description" :label="__('Description')" :placeholder="__('What are you working on?')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($message)
|
||||||
|
<p class="mt-3 text-sm text-zinc-600 dark:text-zinc-300">{{ $message }}</p>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -12,4 +12,14 @@
|
|||||||
@fonts
|
@fonts
|
||||||
|
|
||||||
@vite('resources/themes/backend/assets/css/app.css')
|
@vite('resources/themes/backend/assets/css/app.css')
|
||||||
|
|
||||||
|
|
||||||
|
{{-- Helpdesk owns additional Select2/Flatpickr/Tailwind assets in its module Vite build. --}}
|
||||||
|
@if (file_exists(public_path('build/helpdesk/manifest.json')))
|
||||||
|
@vite([
|
||||||
|
'Resources/assets/css/app.css',
|
||||||
|
'Resources/assets/js/app.js',
|
||||||
|
], 'build/helpdesk')
|
||||||
|
@endif
|
||||||
|
|
||||||
@fluxAppearance
|
@fluxAppearance
|
||||||
|
|||||||
+11
-6
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import { defineConfig } from 'vite';
|
||||||
defineConfig
|
|
||||||
} from 'vite';
|
|
||||||
import laravel from 'laravel-vite-plugin';
|
import laravel from 'laravel-vite-plugin';
|
||||||
import { bunny } from 'laravel-vite-plugin/fonts';
|
import { bunny } from 'laravel-vite-plugin/fonts';
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -12,7 +10,11 @@ export default defineConfig({
|
|||||||
'resources/themes/backend/assets/css/app.css',
|
'resources/themes/backend/assets/css/app.css',
|
||||||
'resources/themes/backend/assets/js/passkeys.js',
|
'resources/themes/backend/assets/js/passkeys.js',
|
||||||
],
|
],
|
||||||
refresh: true,
|
refresh: [
|
||||||
|
'resources/themes/backend/views/**/*.blade.php',
|
||||||
|
'resources/themes/backend/assets/**/*',
|
||||||
|
'Modules/Helpdesk/**/*.php',
|
||||||
|
],
|
||||||
fonts: [
|
fonts: [
|
||||||
bunny('Instrument Sans', {
|
bunny('Instrument Sans', {
|
||||||
weights: [400, 500, 600],
|
weights: [400, 500, 600],
|
||||||
@@ -21,10 +23,13 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
cors: true,
|
cors: true,
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: [
|
||||||
|
'**/storage/framework/views/**',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user