Improve ticketing settings
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user