Files
dashboard-ticketing/resources/js/Pages/Ticketing/Index.vue
2026-04-08 14:17:26 -07:00

198 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<AppLayout title="Ticketing">
<div class="p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Tickets</h1>
<Link
:href="route('ticketing.create')"
class="inline-flex items-center px-4 py-2 text-white rounded-lg transition"
style="background-color: var(--color-sidebar-active-bg)"
>
+ New Ticket
</Link>
</div>
<!-- Filters -->
<div class="mb-4 flex flex-wrap items-center gap-3">
<div class="relative flex-1 min-w-[200px] max-w-md">
<input
v-model="searchInput"
type="text"
placeholder="Search tickets…"
class="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
@keydown.enter="applyFilters"
/>
</div>
<select v-model="filters.status" @change="applyFilters" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400">
<option value="">All Statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
<select v-model="filters.priority" @change="applyFilters" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400">
<option value="">All Priorities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
<select v-model="filters.category" @change="applyFilters" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400">
<option value="">All Categories</option>
<option value="IT">IT</option>
<option value="Facilities">Facilities</option>
<option value="HR">HR</option>
<option value="Other">Other</option>
</select>
<button
@click="applyFilters"
class="px-4 py-2 text-sm text-white rounded-lg transition"
style="background-color: var(--color-sidebar-active-bg)"
>Search</button>
<button
v-if="hasActiveFilters"
@click="clearFilters"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition"
> Clear</button>
</div>
<div class="bg-white rounded-xl shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-600 uppercase text-xs">
<tr>
<th class="px-6 py-3 text-left">#</th>
<th class="px-6 py-3 text-left">Title</th>
<th class="px-6 py-3 text-left">Category</th>
<th class="px-6 py-3 text-left">Priority</th>
<th class="px-6 py-3 text-left">Status</th>
<th v-if="isAdmin" class="px-6 py-3 text-left">Submitter</th>
<th class="px-6 py-3 text-left">Created</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-if="tickets.data.length === 0">
<td :colspan="isAdmin ? 8 : 7" class="px-6 py-10 text-center text-gray-400">
No tickets found.
</td>
</tr>
<tr
v-for="ticket in tickets.data"
:key="ticket.id"
class="hover:bg-gray-50 transition"
>
<td class="px-6 py-4 text-gray-500">{{ ticket.id }}</td>
<td class="px-6 py-4 font-medium text-gray-900 max-w-xs truncate">{{ ticket.title }}</td>
<td class="px-6 py-4 text-gray-600">{{ ticket.category }}</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded text-xs font-medium" :class="priorityClass(ticket.priority)">
{{ ticket.priority }}
</span>
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 rounded text-xs font-medium" :class="statusClass(ticket.status)">
{{ formatStatus(ticket.status) }}
</span>
</td>
<td v-if="isAdmin" class="px-6 py-4 text-gray-600">{{ ticket.submitter?.name ?? '' }}</td>
<td class="px-6 py-4 text-gray-500">{{ formatDate(ticket.created_at) }}</td>
<td class="px-6 py-4 text-right space-x-3">
<Link :href="route('ticketing.show', ticket.id)" class="text-xs text-indigo-600 hover:underline">View</Link>
<Link :href="route('ticketing.edit', ticket.id)" class="text-xs text-gray-500 hover:underline">Edit</Link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div v-if="tickets.last_page > 1" class="mt-4 flex items-center justify-between">
<p class="text-sm text-gray-600">
Showing {{ tickets.from }}{{ tickets.to }} of {{ tickets.total }} tickets
</p>
<div class="flex gap-2">
<Link v-if="tickets.prev_page_url" :href="tickets.prev_page_url" class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50 transition"> Prev</Link>
<Link v-if="tickets.next_page_url" :href="tickets.next_page_url" class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50 transition">Next </Link>
</div>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Link, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
const props = defineProps({
tickets: Object,
search: { type: String, default: '' },
statusFilter: { type: String, default: '' },
priorityFilter: { type: String, default: '' },
categoryFilter: { type: String, default: '' },
isAdmin: { type: Boolean, default: false },
})
const searchInput = ref(props.search)
const filters = ref({
status: props.statusFilter,
priority: props.priorityFilter,
category: props.categoryFilter,
})
const hasActiveFilters = computed(() =>
searchInput.value || filters.value.status || filters.value.priority || filters.value.category
)
function applyFilters() {
router.get(route('ticketing.index'), {
search: searchInput.value,
status: filters.value.status,
priority: filters.value.priority,
category: filters.value.category,
}, { preserveState: true, replace: true })
}
function clearFilters() {
searchInput.value = ''
filters.value = { status: '', priority: '', category: '' }
router.get(route('ticketing.index'), {}, { preserveState: true, replace: true })
}
function formatDate(d) {
return new Date(d).toLocaleDateString('en-CA')
}
function formatStatus(s) {
const map = { open: 'Open', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed' }
return map[s] ?? s
}
function statusClass(s) {
const map = {
open: 'bg-blue-100 text-blue-700',
in_progress: 'bg-yellow-100 text-yellow-700',
resolved: 'bg-green-100 text-green-700',
closed: 'bg-gray-100 text-gray-600',
}
return map[s] ?? 'bg-gray-100 text-gray-600'
}
function priorityClass(p) {
const map = {
low: 'bg-gray-100 text-gray-600',
medium: 'bg-blue-100 text-blue-700',
high: 'bg-orange-100 text-orange-700',
urgent: 'bg-red-100 text-red-700',
}
return map[p] ?? 'bg-gray-100 text-gray-600'
}
</script>