Improve ticketing settings

This commit is contained in:
Joel Wedemire
2026-04-08 19:48:32 -07:00
parent f2b614abb7
commit 3c65f9a4fd
3 changed files with 172 additions and 22 deletions

View File

@@ -5,7 +5,6 @@
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">Ticketing Settings</h1>
</div>
<!-- Bootstrap banner -->
<div v-if="isBootstrap" class="mb-6 px-5 py-4 bg-amber-50 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-600 rounded-xl">
<h2 class="text-base font-semibold text-amber-800 dark:text-amber-200 mb-1">🚀 First-Run Setup</h2>
<p class="text-sm text-amber-700 dark:text-amber-300">
@@ -14,12 +13,14 @@
</p>
</div>
<!-- Flash message -->
<div v-if="$page.props.flash?.success" class="mb-4 px-4 py-2 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-700 dark:text-green-300 rounded-lg text-sm">
{{ $page.props.flash.success }}
</div>
<!-- Tabs -->
<div v-if="$page.props.errors?.priority" class="mb-4 px-4 py-2 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 rounded-lg text-sm">
{{ $page.props.errors.priority }}
</div>
<div class="flex gap-1 border-b border-gray-200 dark:border-gray-700 mb-6">
<button
v-for="tab in tabs"
@@ -34,7 +35,6 @@
>{{ tab.label }}</button>
</div>
<!-- Groups Tab -->
<div v-if="activeTab === 'groups'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Groups</h2>
@@ -43,7 +43,6 @@
</button>
</div>
<!-- Add Group Form -->
<div v-if="showAddGroup" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">New Group</h3>
<form @submit.prevent="submitGroup" class="grid grid-cols-2 gap-3">
@@ -71,7 +70,6 @@
</form>
</div>
<!-- Groups List -->
<div class="space-y-3">
<div v-if="groups.length === 0" class="text-sm text-gray-400 italic">No groups yet.</div>
<div
@@ -90,7 +88,6 @@
</div>
</div>
<!-- Edit Group Form -->
<div v-if="editingGroup" class="mt-4 bg-gray-50 dark:bg-gray-700 rounded-xl p-4 space-y-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Edit: {{ editingGroup.name }}</h3>
<form @submit.prevent="submitEditGroup" class="grid grid-cols-2 gap-3">
@@ -118,7 +115,6 @@
</div>
</div>
<!-- Agents Tab -->
<div v-if="activeTab === 'agents'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Agents</h2>
@@ -127,7 +123,6 @@
</button>
</div>
<!-- Add Agent Form -->
<div v-if="showAddAgent" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<form @submit.prevent="submitAgent" class="grid grid-cols-3 gap-3">
<div>
@@ -156,7 +151,6 @@
</form>
</div>
<!-- Agents List -->
<div class="space-y-2">
<div v-if="agents.length === 0" class="text-sm text-gray-400 italic">No agents configured yet.</div>
<div
@@ -168,15 +162,11 @@
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">{{ access.user?.name || 'User #' + access.user_id }}</p>
<p class="text-xs text-gray-400">{{ access.user?.email }} · {{ access.group?.name || 'Unknown Group' }} · <span class="capitalize">{{ access.role }}</span></p>
</div>
<button
@click="removeAgent(access)"
class="text-xs text-red-500 hover:underline"
>Remove</button>
<button @click="openRemoveAgentModal(access)" class="text-xs text-red-500 hover:underline">Remove</button>
</div>
</div>
</div>
<!-- Priorities Tab -->
<div v-if="activeTab === 'priorities'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Priority Levels</h2>
@@ -185,7 +175,6 @@
</button>
</div>
<!-- Add Priority Form -->
<div v-if="showAddPriority" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<form @submit.prevent="submitPriority" class="grid grid-cols-2 gap-3">
<div>
@@ -219,7 +208,6 @@
</form>
</div>
<!-- Priorities List -->
<div class="space-y-2">
<div v-if="priorities.length === 0" class="text-sm text-gray-400 italic">No priorities defined yet.</div>
<div
@@ -233,9 +221,70 @@
<p v-if="p.description" class="text-xs text-gray-400">{{ p.description }}</p>
</div>
<span class="text-xs text-gray-400">{{ p.group_id ? 'Group-specific' : 'Global' }}</span>
<div class="flex items-center gap-3">
<button @click="startEditPriority(p)" class="text-xs text-indigo-600 hover:underline">Edit</button>
<button @click="openDeletePriorityModal(p)" class="text-xs text-red-500 hover:underline">Delete</button>
</div>
</div>
</div>
<div v-if="editingPriority" class="mt-4 bg-gray-50 dark:bg-gray-700 rounded-xl p-4 space-y-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Edit Priority: {{ editingPriority.name }}</h3>
<form @submit.prevent="submitEditPriority" class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="editPriorityForm.name" required type="text" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
<input v-model="editPriorityForm.color" type="color" class="h-9 w-full border-gray-300 rounded-lg cursor-pointer" />
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Description</label>
<input v-model="editPriorityForm.description" type="text" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Sort Order</label>
<input v-model="editPriorityForm.sort_order" type="number" min="0" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Scope</label>
<input :value="editingPriority.group_id ? 'Group-specific' : 'Global'" disabled type="text" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800/60 dark:text-white rounded-lg opacity-70" />
</div>
<div class="col-span-2 flex justify-end gap-2">
<button type="button" @click="editingPriority = null" class="text-sm text-gray-500 px-3 py-2 rounded-lg border border-gray-300 hover:bg-gray-100">Cancel</button>
<button type="submit" :disabled="editPriorityForm.processing" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700 disabled:opacity-60">Save</button>
</div>
</form>
</div>
</div>
</div>
<div v-if="removeAgentModalOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div class="w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-xl">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Remove agent?</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
Remove <strong>{{ pendingAgentRemoval?.user?.name || ('User #' + pendingAgentRemoval?.user_id) }}</strong>
from <strong>{{ pendingAgentRemoval?.group?.name }}</strong>?
</p>
<div class="mt-6 flex justify-end gap-2">
<button type="button" @click="closeRemoveAgentModal" class="text-sm text-gray-600 px-3 py-2 rounded-lg border border-gray-300 hover:bg-gray-100">Cancel</button>
<button type="button" @click="confirmRemoveAgent" class="bg-red-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-red-700">Remove</button>
</div>
</div>
</div>
<div v-if="deletePriorityModalOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div class="w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-6 shadow-xl">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Delete priority?</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
Delete <strong>{{ pendingPriorityDelete?.name }}</strong>? This cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-2">
<button type="button" @click="closeDeletePriorityModal" class="text-sm text-gray-600 px-3 py-2 rounded-lg border border-gray-300 hover:bg-gray-100">Cancel</button>
<button type="button" @click="confirmDeletePriority" class="bg-red-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-red-700">Delete</button>
</div>
</div>
</div>
</template>
@@ -263,12 +312,17 @@ const showAddGroup = ref(false)
const showAddAgent = ref(false)
const showAddPriority = ref(false)
const editingGroup = ref(null)
const editingPriority = ref(null)
const removeAgentModalOpen = ref(false)
const pendingAgentRemoval = ref(null)
const deletePriorityModalOpen = ref(false)
const pendingPriorityDelete = ref(null)
// Forms
const groupForm = useForm({ name: '', email_address: '', color: '#6366f1', prefix: '' })
const editGroupForm = useForm({ name: '', email_address: '', color: '#6366f1', prefix: '' })
const agentForm = useForm({ user_id: '', group_id: '', role: 'agent' })
const priorityForm = useForm({ name: '', color: '#6b7280', description: '', sort_order: 0, group_id: null })
const editPriorityForm = useForm({ name: '', color: '#6b7280', description: '', sort_order: 0, group_id: null })
function submitGroup() {
groupForm.post(route('ticketing.settings.groups.store'), {
@@ -296,10 +350,22 @@ function submitAgent() {
})
}
function removeAgent(access) {
if (confirm('Remove this agent?')) {
router.delete(route('ticketing.settings.agents.destroy', { access: access.id }))
}
function openRemoveAgentModal(access) {
pendingAgentRemoval.value = access
removeAgentModalOpen.value = true
}
function closeRemoveAgentModal() {
removeAgentModalOpen.value = false
pendingAgentRemoval.value = null
}
function confirmRemoveAgent() {
if (!pendingAgentRemoval.value) return
router.delete(route('ticketing.settings.agents.destroy', { access: pendingAgentRemoval.value.id }), {
onFinish: closeRemoveAgentModal,
})
}
function submitPriority() {
@@ -307,4 +373,37 @@ function submitPriority() {
onSuccess: () => { showAddPriority.value = false; priorityForm.reset() }
})
}
function startEditPriority(priority) {
editingPriority.value = priority
editPriorityForm.name = priority.name
editPriorityForm.color = priority.color
editPriorityForm.description = priority.description || ''
editPriorityForm.sort_order = priority.sort_order
editPriorityForm.group_id = priority.group_id
}
function submitEditPriority() {
editPriorityForm.put(route('ticketing.settings.priorities.update', { priority: editingPriority.value.id }), {
onSuccess: () => { editingPriority.value = null }
})
}
function openDeletePriorityModal(priority) {
pendingPriorityDelete.value = priority
deletePriorityModalOpen.value = true
}
function closeDeletePriorityModal() {
deletePriorityModalOpen.value = false
pendingPriorityDelete.value = null
}
function confirmDeletePriority() {
if (!pendingPriorityDelete.value) return
router.delete(route('ticketing.settings.priorities.destroy', { priority: pendingPriorityDelete.value.id }), {
onFinish: closeDeletePriorityModal,
})
}
</script>

View File

@@ -5,6 +5,7 @@ namespace Dashboard\Ticketing\Http\Controllers;
use Dashboard\Ticketing\Models\PriorityLevel;
use Dashboard\Ticketing\Models\TicketingAgentAccess;
use Dashboard\Ticketing\Models\TicketingGroup;
use Illuminate\Validation\Rule;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
@@ -212,4 +213,52 @@ class TicketingSettingsController extends Controller
return back()->with('success', 'Priority level created.');
}
public function updatePriority(Request $request, PriorityLevel $priority)
{
$this->requireAgentAccess();
if ($priority->group_id) {
$this->requireManagerAccess($priority->group_id);
}
$validated = $request->validate([
'name' => 'required|string|max:100',
'color' => 'required|string|regex:/^#[0-9a-fA-F]{6}$/',
'description' => 'nullable|string',
'sort_order' => 'required|integer|min:0',
'group_id' => [
'nullable',
'exists:ticketing_groups,id',
Rule::in([$priority->group_id, null]),
],
]);
if (!empty($validated['group_id'])) {
$this->requireManagerAccess($validated['group_id']);
}
$priority->update($validated);
return back()->with('success', 'Priority level updated.');
}
public function destroyPriority(PriorityLevel $priority)
{
$this->requireAgentAccess();
if ($priority->group_id) {
$this->requireManagerAccess($priority->group_id);
}
if ($priority->tickets()->exists()) {
return back()->withErrors([
'priority' => 'Cannot delete a priority that is in use by tickets.',
]);
}
$priority->delete();
return back()->with('success', 'Priority level removed.');
}
}

View File

@@ -17,6 +17,8 @@ Route::middleware(['web', 'auth', 'app.access:ticketing'])->prefix('app/ticketin
Route::post('/settings/agents', [TicketingSettingsController::class, 'storeAgent'])->name('settings.agents.store');
Route::delete('/settings/agents/{access}', [TicketingSettingsController::class, 'destroyAgent'])->name('settings.agents.destroy');
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority'])->name('settings.priorities.store');
Route::put('/settings/priorities/{priority}', [TicketingSettingsController::class, 'updatePriority'])->name('settings.priorities.update');
Route::delete('/settings/priorities/{priority}', [TicketingSettingsController::class, 'destroyPriority'])->name('settings.priorities.destroy');
// Ticket routes
Route::get('/', [TicketController::class, 'index'])->name('index');