fix(ticketing): restrict settings link to admins and protect global priorities

- Show 'Go to Settings' bootstrap link only for admin/super_admin users
- Pass isSiteAdmin prop to Create.vue to control settings CTA visibility
- Require site admin for updatePriority/destroyPriority when priority is global (group_id = null)
- Closes: non-admin users seeing forbidden settings link; agents mutating global priorities
This commit is contained in:
Joel Wedemire
2026-04-09 14:32:19 -07:00
parent 45b019dad2
commit bce98c0d4b
7 changed files with 493 additions and 473 deletions

View File

@@ -1,103 +1,107 @@
<template>
<div class="max-w-2xl mx-auto py-8 px-4">
<div class="mb-6">
<Link :href="route('ticketing.index')" class="text-sm text-indigo-600 hover:underline"> Back to tickets</Link>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">Submit a Ticket</h1>
<AppLayout>
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<Link :href="route('ticketing.index')" class="text-sm text-indigo-600 hover:underline"> Back to tickets</Link>
<h1 class="text-2xl font-bold text-gray-900 mt-2">Submit a Ticket</h1>
</div>
<!-- Bootstrap / No groups state -->
<div v-if="isBootstrap" class="bg-amber-50 border border-amber-300 rounded-xl px-5 py-6 text-center">
<p class="text-amber-800 font-semibold text-base mb-2">📦 Ticketing isn't set up yet</p>
<p class="text-sm text-amber-700 mb-4">An admin needs to create at least one group before tickets can be submitted.</p>
<Link v-if="isSiteAdmin" :href="route('ticketing.settings')" class="inline-block bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700">Go to Settings</Link>
</div>
<form v-else @submit.prevent="submit" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-5">
<!-- Group -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Group <span class="text-red-500">*</span></label>
<select
v-model="form.group_id"
required
class="w-full border-gray-300 rounded-lg text-sm"
>
<option value="">Select a group</option>
<option v-for="g in groups" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
<p v-if="form.errors.group_id" class="text-xs text-red-600 mt-1">{{ form.errors.group_id }}</p>
</div>
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Title <span class="text-red-500">*</span></label>
<input
v-model="form.title"
type="text"
required
placeholder="Brief summary of the issue"
class="w-full border-gray-300 rounded-lg text-sm"
/>
<p v-if="form.errors.title" class="text-xs text-red-600 mt-1">{{ form.errors.title }}</p>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-red-500">*</span></label>
<textarea
v-model="form.description"
required
rows="5"
placeholder="Describe the issue in detail..."
class="w-full border-gray-300 rounded-lg text-sm"
></textarea>
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">{{ form.errors.description }}</p>
</div>
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select
v-model="form.priority_id"
class="w-full border-gray-300 rounded-lg text-sm"
>
<option :value="null">No priority</option>
<option v-for="p in filteredPriorities" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<!-- Due Date -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Due Date <span class="text-gray-400 font-normal">(optional)</span></label>
<input
v-model="form.due_date"
type="date"
class="w-full border-gray-300 rounded-lg text-sm"
/>
</div>
<!-- Submit -->
<div class="flex justify-end pt-2">
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center gap-2 bg-gray-900 text-white px-5 py-2 rounded-lg text-sm font-medium hover:bg-gray-700 disabled:opacity-60 transition"
>
<span v-if="form.processing">Submitting…</span>
<span v-else>Submit Ticket</span>
</button>
</div>
</form>
</div>
<!-- Bootstrap / No groups state -->
<div v-if="isBootstrap" class="bg-amber-50 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-600 rounded-xl px-5 py-6 text-center">
<p class="text-amber-800 dark:text-amber-200 font-semibold text-base mb-2">📦 Ticketing isnt set up yet</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">An admin needs to create at least one group before tickets can be submitted.</p>
<Link :href="route('ticketing.settings')" class="inline-block bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700">Go to Settings</Link>
</div>
<form v-else @submit.prevent="submit" class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-5">
<!-- Group -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Group <span class="text-red-500">*</span></label>
<select
v-model="form.group_id"
required
class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"
>
<option value="">Select a group</option>
<option v-for="g in groups" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
<p v-if="form.errors.group_id" class="text-xs text-red-600 mt-1">{{ form.errors.group_id }}</p>
</div>
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title <span class="text-red-500">*</span></label>
<input
v-model="form.title"
type="text"
required
placeholder="Brief summary of the issue"
class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"
/>
<p v-if="form.errors.title" class="text-xs text-red-600 mt-1">{{ form.errors.title }}</p>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description <span class="text-red-500">*</span></label>
<textarea
v-model="form.description"
required
rows="5"
placeholder="Describe the issue in detail..."
class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"
></textarea>
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">{{ form.errors.description }}</p>
</div>
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Priority</label>
<select
v-model="form.priority_id"
class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"
>
<option :value="null">No priority</option>
<option v-for="p in filteredPriorities" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<!-- Due Date -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Due Date <span class="text-gray-400 font-normal">(optional)</span></label>
<input
v-model="form.due_date"
type="date"
class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"
/>
</div>
<!-- Submit -->
<div class="flex justify-end pt-2">
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center gap-2 bg-indigo-600 text-white px-5 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 disabled:opacity-60 transition"
>
<span v-if="form.processing">Submitting</span>
<span v-else>Submit Ticket</span>
</button>
</div>
</form>
</div>
</AppLayout>
</template>
<script setup>
import { computed, watch } from 'vue'
import { Link, useForm } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
const props = defineProps({
groups: Array,
priorities: Array,
isBootstrap: Boolean,
isSiteAdmin: Boolean,
})
const form = useForm({

View File

@@ -1,90 +1,93 @@
<template>
<div class="max-w-2xl mx-auto py-8 px-4">
<div class="mb-6">
<Link :href="route('ticketing.show', { ticket: ticket.id })" class="text-sm text-indigo-600 hover:underline"> Back to ticket</Link>
<h1 class="text-xl font-bold text-gray-900 dark:text-white mt-2">
Edit <span class="font-mono text-base">{{ ticket.number }}</span>
</h1>
<AppLayout>
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<Link :href="route('ticketing.show', { ticket: ticket.id })" class="text-sm text-indigo-600 hover:underline"> Back to ticket</Link>
<h1 class="text-xl font-bold text-gray-900 mt-2">
Edit <span class="font-mono text-base">{{ ticket.number }}</span>
</h1>
</div>
<form @submit.prevent="submit" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-5">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input v-model="form.title" type="text" required class="w-full border-gray-300 rounded-lg text-sm" />
<p v-if="form.errors.title" class="text-xs text-red-600 mt-1">{{ form.errors.title }}</p>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea v-model="form.description" rows="6" required class="w-full border-gray-300 rounded-lg text-sm"></textarea>
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">{{ form.errors.description }}</p>
</div>
<!-- Status -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select v-model="form.status" class="w-full border-gray-300 rounded-lg text-sm">
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="pending">Pending</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
</div>
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select v-model="form.priority_id" class="w-full border-gray-300 rounded-lg text-sm">
<option :value="null">No priority</option>
<option v-for="p in priorities" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<!-- Assignee -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Assignee</label>
<select v-model="form.assigned_to" class="w-full border-gray-300 rounded-lg text-sm">
<option :value="null">Unassigned</option>
<option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
</div>
<!-- Project -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Project</label>
<select v-model="form.project_id" class="w-full border-gray-300 rounded-lg text-sm">
<option :value="null">No project</option>
<option v-for="proj in projects" :key="proj.id" :value="proj.id">{{ proj.name }}</option>
</select>
</div>
<!-- Due Date -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Due Date</label>
<input v-model="form.due_date" type="date" class="w-full border-gray-300 rounded-lg text-sm" />
</div>
<div class="flex gap-3 justify-end pt-2">
<Link
:href="route('ticketing.show', { ticket: ticket.id })"
class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
>Cancel</Link>
<button
type="submit"
:disabled="form.processing"
class="px-5 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-700 disabled:opacity-60 transition"
>
{{ form.processing ? 'Saving…' : 'Save Changes' }}
</button>
</div>
</form>
</div>
<form @submit.prevent="submit" class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-5">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
<input v-model="form.title" type="text" required class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm" />
<p v-if="form.errors.title" class="text-xs text-red-600 mt-1">{{ form.errors.title }}</p>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea v-model="form.description" rows="6" required class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm"></textarea>
<p v-if="form.errors.description" class="text-xs text-red-600 mt-1">{{ form.errors.description }}</p>
</div>
<!-- Status -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
<select v-model="form.status" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm">
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="pending">Pending</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
</div>
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Priority</label>
<select v-model="form.priority_id" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm">
<option :value="null">No priority</option>
<option v-for="p in priorities" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<!-- Assignee -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Assignee</label>
<select v-model="form.assigned_to" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm">
<option :value="null">Unassigned</option>
<option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
</div>
<!-- Project -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project</label>
<select v-model="form.project_id" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm">
<option :value="null">No project</option>
<option v-for="proj in projects" :key="proj.id" :value="proj.id">{{ proj.name }}</option>
</select>
</div>
<!-- Due Date -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Due Date</label>
<input v-model="form.due_date" type="date" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg text-sm" />
</div>
<div class="flex gap-3 justify-end pt-2">
<Link
:href="route('ticketing.show', { ticket: ticket.id })"
class="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
>Cancel</Link>
<button
type="submit"
:disabled="form.processing"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 disabled:opacity-60 transition"
>
{{ form.processing ? 'Saving…' : 'Save Changes' }}
</button>
</div>
</form>
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import { Link, useForm } from '@inertiajs/vue3'
const props = defineProps({

View File

@@ -1,46 +1,49 @@
<template>
<div class="max-w-3xl mx-auto py-8 px-4">
<div class="mb-6 flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">My Tickets</h1>
<Link :href="route('ticketing.create')" class="inline-flex items-center gap-1 bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700 transition">
+ Submit Ticket
</Link>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700">
<div v-if="tickets.data.length === 0" class="p-8 text-center text-gray-400 text-sm">
You haven't submitted any tickets yet.
<AppLayout>
<div class="max-w-3xl mx-auto">
<div class="mb-6 flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900">My Tickets</h1>
<Link :href="route('ticketing.create')" class="inline-flex items-center gap-1 bg-gray-900 text-white text-sm px-4 py-2 rounded-lg hover:bg-gray-700 transition">
+ Submit Ticket
</Link>
</div>
<Link
v-for="ticket in tickets.data"
:key="ticket.id"
:href="route('ticketing.show', { ticket: ticket.id })"
class="flex items-start gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition"
>
<span
class="inline-block text-xs font-mono font-semibold px-2 py-1 rounded text-white flex-shrink-0 mt-0.5"
:style="{ backgroundColor: ticket.group?.color || '#6366f1' }"
>{{ ticket.number }}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">{{ ticket.title }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ ticket.group?.name }} · {{ timeAgo(ticket.created_at) }}</p>
</div>
<span :class="statusClass(ticket.status)" class="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0">
{{ statusLabel(ticket.status) }}
</span>
</Link>
</div>
<!-- Pagination -->
<div v-if="tickets.last_page > 1" class="mt-4 flex justify-center gap-4 text-sm">
<Link v-if="tickets.prev_page_url" :href="tickets.prev_page_url" class="text-indigo-600 hover:underline"> Previous</Link>
<span class="text-gray-500">{{ tickets.current_page }} / {{ tickets.last_page }}</span>
<Link v-if="tickets.next_page_url" :href="tickets.next_page_url" class="text-indigo-600 hover:underline">Next </Link>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 divide-y divide-gray-100">
<div v-if="tickets.data.length === 0" class="p-8 text-center text-gray-400 text-sm">
You haven't submitted any tickets yet.
</div>
<Link
v-for="ticket in tickets.data"
:key="ticket.id"
:href="route('ticketing.show', { ticket: ticket.id })"
class="flex items-start gap-3 px-4 py-3 hover:bg-gray-50 transition"
>
<span
class="inline-block text-xs font-mono font-semibold px-2 py-1 rounded text-white flex-shrink-0 mt-0.5"
:style="{ backgroundColor: ticket.group?.color || '#6366f1' }"
>{{ ticket.number }}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-800 truncate">{{ ticket.title }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ ticket.group?.name }} · {{ timeAgo(ticket.created_at) }}</p>
</div>
<span :class="statusClass(ticket.status)" class="text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0">
{{ statusLabel(ticket.status) }}
</span>
</Link>
</div>
<!-- Pagination -->
<div v-if="tickets.last_page > 1" class="mt-4 flex justify-center gap-4 text-sm">
<Link v-if="tickets.prev_page_url" :href="tickets.prev_page_url" class="text-indigo-600 hover:underline"> Previous</Link>
<span class="text-gray-500">{{ tickets.current_page }} / {{ tickets.last_page }}</span>
<Link v-if="tickets.next_page_url" :href="tickets.next_page_url" class="text-indigo-600 hover:underline">Next </Link>
</div>
</div>
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import { Link } from '@inertiajs/vue3'
defineProps({ tickets: Object })
@@ -52,7 +55,7 @@ function statusLabel(status) {
function statusClass(status) {
const map = {
open: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200',
open: 'bg-blue-100 text-blue-700',
in_progress: 'bg-purple-100 text-purple-700',
pending: 'bg-yellow-100 text-yellow-700',
resolved: 'bg-green-100 text-green-700',

View File

@@ -2,30 +2,30 @@
<div class="max-w-5xl mx-auto py-8 px-4">
<div class="mb-6">
<Link :href="route('ticketing.index')" class="text-sm text-indigo-600 hover:underline"> Back to tickets</Link>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mt-2">Ticketing Settings</h1>
<h1 class="text-2xl font-bold text-gray-900 mt-2">Ticketing Settings</h1>
</div>
<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">
<div v-if="isBootstrap" class="mb-6 px-5 py-4 bg-amber-50 border border-amber-300 rounded-xl">
<h2 class="text-base font-semibold text-amber-800 mb-1">🚀 First-Run Setup</h2>
<p class="text-sm text-amber-700">
No groups exist yet. Create your first group below to get started.
Default priorities (Low, Medium, High, Urgent) will be seeded automatically.
</p>
</div>
<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">
<div v-if="$page.props.flash?.success" class="mb-4 px-4 py-2 bg-green-50 border border-green-200 text-green-700 rounded-lg text-sm">
{{ $page.props.flash.success }}
</div>
<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">
<div v-if="$page.props.errors?.priority" class="mb-4 px-4 py-2 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{{ $page.props.errors.priority }}
</div>
<div v-if="$page.props.errors?.project" 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">
<div v-if="$page.props.errors?.project" class="mb-4 px-4 py-2 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{{ $page.props.errors.project }}
</div>
<div class="flex gap-1 border-b border-gray-200 dark:border-gray-700 mb-6">
<div class="flex gap-1 border-b border-gray-200 mb-6">
<button
v-for="tab in tabs"
:key="tab.key"
@@ -33,34 +33,34 @@
:class="[
'px-4 py-2.5 text-sm font-medium border-b-2 transition',
activeTab === tab.key
? 'border-indigo-600 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700'
? 'border-indigo-600 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
]"
>{{ tab.label }}</button>
</div>
<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>
<h2 class="text-lg font-semibold text-gray-800">Groups</h2>
<button @click="showAddGroup = !showAddGroup" class="text-sm text-indigo-600 hover:underline">
{{ showAddGroup ? 'Cancel' : '+ Add Group' }}
</button>
</div>
<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>
<div v-if="showAddGroup" class="bg-gray-50 rounded-xl p-4 mb-5 space-y-3">
<h3 class="text-sm font-semibold text-gray-700">New Group</h3>
<form @submit.prevent="submitGroup" class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="groupForm.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" />
<input v-model="groupForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Prefix (e.g. IT)</label>
<input v-model="groupForm.prefix" required type="text" maxlength="10" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg uppercase" />
<input v-model="groupForm.prefix" required type="text" maxlength="10" class="w-full text-sm border-gray-300 rounded-lg uppercase" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Email Address</label>
<input v-model="groupForm.email_address" type="email" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
<input v-model="groupForm.email_address" type="email" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
@@ -79,12 +79,12 @@
<div
v-for="group in groups"
:key="group.id"
class="flex items-center justify-between px-4 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl"
class="flex items-center justify-between px-4 py-3 bg-white border border-gray-200 rounded-xl"
>
<div class="flex items-center gap-3">
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: group.color }"></span>
<div>
<p class="font-medium text-sm text-gray-800 dark:text-gray-100">{{ group.name }}</p>
<p class="font-medium text-sm text-gray-800">{{ group.name }}</p>
<p class="text-xs text-gray-400">{{ group.prefix }} · {{ group.email_address || 'No email' }}</p>
</div>
</div>
@@ -92,20 +92,20 @@
</div>
</div>
<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>
<div v-if="editingGroup" class="mt-4 bg-gray-50 rounded-xl p-4 space-y-3">
<h3 class="text-sm font-semibold text-gray-700">Edit: {{ editingGroup.name }}</h3>
<form @submit.prevent="submitEditGroup" class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="editGroupForm.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" />
<input v-model="editGroupForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Prefix</label>
<input v-model="editGroupForm.prefix" required type="text" maxlength="10" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg uppercase" />
<input v-model="editGroupForm.prefix" required type="text" maxlength="10" class="w-full text-sm border-gray-300 rounded-lg uppercase" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Email Address</label>
<input v-model="editGroupForm.email_address" type="email" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
<input v-model="editGroupForm.email_address" type="email" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
@@ -121,28 +121,28 @@
<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>
<h2 class="text-lg font-semibold text-gray-800">Agents</h2>
<button @click="showAddAgent = !showAddAgent" class="text-sm text-indigo-600 hover:underline">
{{ showAddAgent ? 'Cancel' : '+ Add Agent' }}
</button>
</div>
<div v-if="showAddAgent" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<div v-if="showAddAgent" class="bg-gray-50 rounded-xl p-4 mb-5 space-y-3">
<form @submit.prevent="submitAgent" class="grid grid-cols-3 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">User ID</label>
<input v-model="agentForm.user_id" required type="number" placeholder="User ID" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
<input v-model="agentForm.user_id" required type="number" placeholder="User ID" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Group</label>
<select v-model="agentForm.group_id" required class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="agentForm.group_id" required class="w-full text-sm border-gray-300 rounded-lg">
<option value="">Select group</option>
<option v-for="g in groups" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Role</label>
<select v-model="agentForm.role" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="agentForm.role" class="w-full text-sm border-gray-300 rounded-lg">
<option value="agent">Agent</option>
<option value="manager">Manager</option>
</select>
@@ -160,10 +160,10 @@
<div
v-for="access in agents"
:key="access.id"
class="flex items-center justify-between px-4 py-2.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl"
class="flex items-center justify-between px-4 py-2.5 bg-white border border-gray-200 rounded-xl"
>
<div>
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">{{ access.user?.name || 'User #' + access.user_id }}</p>
<p class="text-sm font-medium text-gray-800">{{ 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="openRemoveAgentModal(access)" class="text-xs text-red-500 hover:underline">Remove</button>
@@ -173,17 +173,17 @@
<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>
<h2 class="text-lg font-semibold text-gray-800">Priority Levels</h2>
<button @click="showAddPriority = !showAddPriority" class="text-sm text-indigo-600 hover:underline">
{{ showAddPriority ? 'Cancel' : '+ Add Priority' }}
</button>
</div>
<div v-if="showAddPriority" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<div v-if="showAddPriority" class="bg-gray-50 rounded-xl p-4 mb-5 space-y-3">
<form @submit.prevent="submitPriority" class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="priorityForm.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" />
<input v-model="priorityForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
@@ -191,15 +191,15 @@
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Description</label>
<input v-model="priorityForm.description" type="text" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg" />
<input v-model="priorityForm.description" type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Sort Order</label>
<input v-model="priorityForm.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" />
<input v-model="priorityForm.sort_order" type="number" min="0" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Group (blank = global)</label>
<select v-model="priorityForm.group_id" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="priorityForm.group_id" class="w-full text-sm border-gray-300 rounded-lg">
<option :value="null">Global</option>
<option v-for="g in groups" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
@@ -217,11 +217,11 @@
<div
v-for="p in priorities"
:key="p.id"
class="flex items-center gap-3 px-4 py-2.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl"
class="flex items-center gap-3 px-4 py-2.5 bg-white border border-gray-200 rounded-xl"
>
<span class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: p.color }"></span>
<div class="flex-1">
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">{{ p.name }}</p>
<p class="text-sm font-medium text-gray-800">{{ p.name }}</p>
<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>
@@ -232,12 +232,12 @@
</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>
<div v-if="editingPriority" class="mt-4 bg-gray-50 rounded-xl p-4 space-y-3">
<h3 class="text-sm font-semibold text-gray-700">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" />
<input v-model="editPriorityForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
@@ -245,15 +245,15 @@
</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" />
<input v-model="editPriorityForm.description" type="text" class="w-full text-sm border-gray-300 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" />
<input v-model="editPriorityForm.sort_order" type="number" min="0" class="w-full text-sm border-gray-300 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" />
<input :value="editingPriority.group_id ? 'Group-specific' : 'Global'" disabled type="text" class="w-full text-sm border-gray-300 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>
@@ -265,35 +265,35 @@
<div v-if="activeTab === 'projects'">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Projects</h2>
<h2 class="text-lg font-semibold text-gray-800">Projects</h2>
<button @click="showAddProject = !showAddProject" class="text-sm text-indigo-600 hover:underline">
{{ showAddProject ? 'Cancel' : '+ Add Project' }}
</button>
</div>
<div v-if="showAddProject" class="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-5 space-y-3">
<div v-if="showAddProject" class="bg-gray-50 rounded-xl p-4 mb-5 space-y-3">
<form @submit.prevent="submitProject" class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Group</label>
<select v-model="projectForm.group_id" required class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="projectForm.group_id" required class="w-full text-sm border-gray-300 rounded-lg">
<option value="">Select group</option>
<option v-for="g in groups" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Status</label>
<select v-model="projectForm.status" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="projectForm.status" class="w-full text-sm border-gray-300 rounded-lg">
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="projectForm.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" />
<input v-model="projectForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Description</label>
<textarea v-model="projectForm.description" rows="3" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg"></textarea>
<textarea v-model="projectForm.description" rows="3" class="w-full text-sm border-gray-300 rounded-lg"></textarea>
</div>
<div class="col-span-2 flex justify-end">
<button type="submit" :disabled="projectForm.processing" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700 disabled:opacity-60">
@@ -308,10 +308,10 @@
<div
v-for="project in projects"
:key="project.id"
class="flex items-center gap-3 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl"
class="flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl"
>
<div class="flex-1">
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">{{ project.name }}</p>
<p class="text-sm font-medium text-gray-800">{{ project.name }}</p>
<p class="text-xs text-gray-400">{{ groups.find(g => g.id === project.group_id)?.name || 'Unknown Group' }} · {{ project.status }}</p>
<p v-if="project.description" class="text-xs text-gray-500 mt-1">{{ project.description }}</p>
</div>
@@ -322,27 +322,27 @@
</div>
</div>
<div v-if="editingProject" 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 Project: {{ editingProject.name }}</h3>
<div v-if="editingProject" class="mt-4 bg-gray-50 rounded-xl p-4 space-y-3">
<h3 class="text-sm font-semibold text-gray-700">Edit Project: {{ editingProject.name }}</h3>
<form @submit.prevent="submitEditProject" class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Name</label>
<input v-model="editProjectForm.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" />
<input v-model="editProjectForm.name" required type="text" class="w-full text-sm border-gray-300 rounded-lg" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Status</label>
<select v-model="editProjectForm.status" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg">
<select v-model="editProjectForm.status" class="w-full text-sm border-gray-300 rounded-lg">
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Group</label>
<input :value="groups.find(g => g.id === editingProject.group_id)?.name || 'Unknown Group'" 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" />
<input :value="groups.find(g => g.id === editingProject.group_id)?.name || 'Unknown Group'" disabled type="text" class="w-full text-sm border-gray-300 rounded-lg opacity-70" />
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">Description</label>
<textarea v-model="editProjectForm.description" rows="3" class="w-full text-sm border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white rounded-lg"></textarea>
<textarea v-model="editProjectForm.description" rows="3" class="w-full text-sm border-gray-300 rounded-lg"></textarea>
</div>
<div class="col-span-2 flex justify-end gap-2">
<button type="button" @click="editingProject = null" class="text-sm text-gray-500 px-3 py-2 rounded-lg border border-gray-300 hover:bg-gray-100">Cancel</button>
@@ -354,9 +354,9 @@
</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">
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
<h3 class="text-lg font-semibold text-gray-900">Remove agent?</h3>
<p class="mt-2 text-sm text-gray-600">
Remove <strong>{{ pendingAgentRemoval?.user?.name || ('User #' + pendingAgentRemoval?.user_id) }}</strong>
from <strong>{{ pendingAgentRemoval?.group?.name }}</strong>?
</p>
@@ -368,9 +368,9 @@
</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">
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
<h3 class="text-lg font-semibold text-gray-900">Delete priority?</h3>
<p class="mt-2 text-sm text-gray-600">
Delete <strong>{{ pendingPriorityDelete?.name }}</strong>? This cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-2">
@@ -381,9 +381,9 @@
</div>
<div v-if="deleteProjectModalOpen" 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 project?</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
<h3 class="text-lg font-semibold text-gray-900">Delete project?</h3>
<p class="mt-2 text-sm text-gray-600">
Delete <strong>{{ pendingProjectDelete?.name }}</strong>? This cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-2">

View File

@@ -1,218 +1,218 @@
<template>
<div class="max-w-4xl mx-auto py-8 px-4">
<!-- Header -->
<div class="mb-6">
<Link :href="route('ticketing.index')" class="text-sm text-indigo-600 hover:underline"> Back to tickets</Link>
</div>
<AppLayout>
<div class="max-w-4xl mx-auto">
<!-- Header -->
<div class="mb-6">
<Link :href="route('ticketing.index')" class="text-sm text-indigo-600 hover:underline"> Back to tickets</Link>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<!-- Ticket Header -->
<div class="p-5 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-start gap-3 flex-wrap">
<span
class="inline-block text-xs font-mono font-semibold px-2 py-1 rounded text-white flex-shrink-0"
:style="{ backgroundColor: ticket.group?.color || '#6366f1' }"
>
{{ ticket.number }}
</span>
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<!-- Ticket Header -->
<div class="p-5 border-b border-gray-200">
<div class="flex items-start gap-3 flex-wrap">
<span
class="inline-block text-xs font-mono font-semibold px-2 py-1 rounded text-white flex-shrink-0"
:style="{ backgroundColor: ticket.group?.color || '#6366f1' }"
>
{{ ticket.number }}
</span>
<!-- Title (editable inline for agents) -->
<div class="flex-1 min-w-0">
<div v-if="editingTitle && isAgent" class="flex items-center gap-2">
<!-- Title (editable inline for agents) -->
<div class="flex-1 min-w-0">
<div v-if="editingTitle && isAgent" class="flex items-center gap-2">
<input
v-model="titleEdit"
class="flex-1 text-xl font-semibold border-b-2 border-indigo-400 bg-transparent focus:outline-none"
@keyup.enter="saveTitle"
@keyup.esc="editingTitle = false"
/>
<button @click="saveTitle" class="text-xs text-green-600 hover:underline">Save</button>
<button @click="editingTitle = false" class="text-xs text-gray-400 hover:underline">Cancel</button>
</div>
<h1
v-else
class="text-xl font-semibold text-gray-900"
:class="{ 'cursor-pointer hover:text-indigo-600': isAgent }"
@click="isAgent && startEditTitle()"
>
{{ ticket.title }}
<span v-if="isAgent" class="ml-1 text-xs text-gray-400"></span>
</h1>
</div>
</div>
<!-- Meta row -->
<div class="flex flex-wrap items-center gap-3 mt-3">
<!-- Status -->
<div>
<select
v-if="isAgent"
v-model="metaForm.status"
@change="saveMeta"
class="text-xs border-0 rounded-full px-3 py-1 font-medium cursor-pointer"
:class="statusClass(metaForm.status)"
>
<option v-for="s in statuses" :key="s.value" :value="s.value">{{ s.label }}</option>
</select>
<span v-else :class="statusClass(ticket.status)" class="inline-block text-xs px-2 py-1 rounded-full font-medium">
{{ statusLabel(ticket.status) }}
</span>
</div>
<!-- Priority -->
<div>
<select
v-if="isAgent"
v-model="metaForm.priority_id"
@change="saveMeta"
class="text-xs border border-gray-200 rounded-full px-3 py-1"
>
<option :value="null">No priority</option>
<option v-for="p in priorities" :key="p.id" :value="p.id">
{{ p.name }}
</option>
</select>
<span v-else-if="ticket.priority" class="inline-flex items-center gap-1 text-xs text-gray-600">
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: ticket.priority.color }"></span>
{{ ticket.priority.name }}
</span>
</div>
<!-- Assignee -->
<div v-if="isAgent">
<select
v-model="metaForm.assigned_to"
@change="saveMeta"
class="text-xs border border-gray-200 rounded-full px-3 py-1"
>
<option :value="null">Unassigned</option>
<option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
</div>
<!-- Due date -->
<div class="flex items-center gap-1 text-xs text-gray-500">
<span>📅</span>
<input
v-model="titleEdit"
class="flex-1 text-xl font-semibold border-b-2 border-indigo-400 bg-transparent dark:text-white focus:outline-none"
@keyup.enter="saveTitle"
@keyup.esc="editingTitle = false"
v-if="isAgent"
v-model="metaForm.due_date"
type="date"
@change="saveMeta"
class="text-xs border-0 bg-transparent cursor-pointer p-0"
/>
<button @click="saveTitle" class="text-xs text-green-600 hover:underline">Save</button>
<button @click="editingTitle = false" class="text-xs text-gray-400 hover:underline">Cancel</button>
<span v-else>{{ ticket.due_date || 'No due date' }}</span>
</div>
<!-- Edit / Delete actions -->
<div class="ml-auto flex gap-2">
<Link
v-if="isAgent"
:href="route('ticketing.edit', { ticket: ticket.id })"
class="text-xs text-indigo-600 hover:underline"
>Edit</Link>
<button
v-if="isManager"
@click="confirmDelete"
class="text-xs text-red-500 hover:underline"
>Delete</button>
</div>
<h1
v-else
class="text-xl font-semibold text-gray-900 dark:text-white"
:class="{ 'cursor-pointer hover:text-indigo-600': isAgent }"
@click="isAgent && startEditTitle()"
>
{{ ticket.title }}
<span v-if="isAgent" class="ml-1 text-xs text-gray-400"></span>
</h1>
</div>
</div>
<!-- Meta row -->
<div class="flex flex-wrap items-center gap-3 mt-3">
<!-- Status -->
<div>
<select
v-if="isAgent"
v-model="metaForm.status"
@change="saveMeta"
class="text-xs border-0 rounded-full px-3 py-1 font-medium cursor-pointer"
:class="statusClass(metaForm.status)"
>
<option v-for="s in statuses" :key="s.value" :value="s.value">{{ s.label }}</option>
</select>
<span v-else :class="statusClass(ticket.status)" class="inline-block text-xs px-2 py-1 rounded-full font-medium">
{{ statusLabel(ticket.status) }}
</span>
<!-- Message Thread -->
<div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
<div v-if="ticket.messages.length === 0" class="text-sm text-gray-400 text-center py-8">
No messages yet.
</div>
<!-- Priority -->
<div>
<select
v-if="isAgent"
v-model="metaForm.priority_id"
@change="saveMeta"
class="text-xs border border-gray-200 dark:border-gray-600 rounded-full px-3 py-1 dark:bg-gray-700 dark:text-white"
>
<option :value="null">No priority</option>
<option v-for="p in priorities" :key="p.id" :value="p.id">
{{ p.name }}
</option>
</select>
<span v-else-if="ticket.priority" class="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300">
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: ticket.priority.color }"></span>
{{ ticket.priority.name }}
</span>
</div>
<!-- Assignee -->
<div v-if="isAgent">
<select
v-model="metaForm.assigned_to"
@change="saveMeta"
class="text-xs border border-gray-200 dark:border-gray-600 rounded-full px-3 py-1 dark:bg-gray-700 dark:text-white"
>
<option :value="null">Unassigned</option>
<option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
</div>
<!-- Due date -->
<div class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
<span>📅</span>
<input
v-if="isAgent"
v-model="metaForm.due_date"
type="date"
@change="saveMeta"
class="text-xs border-0 bg-transparent dark:text-gray-400 cursor-pointer p-0"
/>
<span v-else>{{ ticket.due_date || 'No due date' }}</span>
</div>
<!-- Edit / Delete actions -->
<div class="ml-auto flex gap-2">
<Link
v-if="isAgent"
:href="route('ticketing.edit', { ticket: ticket.id })"
class="text-xs text-indigo-600 hover:underline"
>Edit</Link>
<button
v-if="isManager"
@click="confirmDelete"
class="text-xs text-red-500 hover:underline"
>Delete</button>
</div>
</div>
</div>
<!-- Message Thread -->
<div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
<div v-if="ticket.messages.length === 0" class="text-sm text-gray-400 text-center py-8">
No messages yet.
</div>
<div
v-for="msg in ticket.messages"
:key="msg.id"
:class="[
'flex',
isOwnMessage(msg) ? 'justify-end' : 'justify-start'
]"
>
<div
:class="[
'max-w-[75%] rounded-xl px-4 py-2.5 text-sm',
msg.is_internal
? 'bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700 text-amber-900 dark:text-amber-100'
: isOwnMessage(msg)
? 'bg-indigo-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100'
]"
v-for="msg in ticket.messages"
:key="msg.id"
:class="['flex', isOwnMessage(msg) ? 'justify-end' : 'justify-start']"
>
<div class="flex items-center gap-2 mb-1 text-xs opacity-70">
<span v-if="msg.is_internal">🔒 Internal Note · </span>
<span>{{ msg.author?.name || msg.author_email || 'Unknown' }}</span>
<span>· {{ timeAgo(msg.created_at) }}</span>
</div>
<p class="whitespace-pre-wrap">{{ msg.body }}</p>
</div>
</div>
</div>
<!-- Reply Area -->
<div class="border-t border-gray-200 dark:border-gray-700 p-5">
<!-- Tab switcher (agents only) -->
<div v-if="isAgent" class="flex gap-2 mb-3">
<button
@click="replyTab = 'reply'"
:class="[
'text-xs px-3 py-1.5 rounded-md font-medium transition',
replyTab === 'reply' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200'
]"
>Reply to submitter</button>
<button
@click="replyTab = 'internal'"
:class="[
'text-xs px-3 py-1.5 rounded-md font-medium transition',
replyTab === 'internal' ? 'bg-amber-500 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200'
]"
>🔒 Internal Note</button>
</div>
<form @submit.prevent="sendMessage">
<textarea
v-model="messageForm.body"
required
rows="3"
:placeholder="replyTab === 'internal' ? 'Internal note — only visible to agents…' : 'Type your reply…'"
:class="[
'w-full rounded-lg text-sm border',
replyTab === 'internal'
? 'border-amber-300 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-700'
: 'border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white'
]"
></textarea>
<div class="flex items-center justify-between mt-3">
<label class="flex items-center gap-2 text-xs text-gray-500 cursor-pointer">
<input type="file" class="hidden" @change="attachFile" />
<span class="text-gray-400 hover:text-indigo-600 transition">📎 Attach file</span>
</label>
<button
type="submit"
:disabled="messageForm.processing"
<div
:class="[
'inline-flex items-center gap-1 text-sm font-medium px-4 py-2 rounded-lg transition',
replyTab === 'internal'
? 'bg-amber-500 text-white hover:bg-amber-600 disabled:opacity-60'
: 'bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-60'
'max-w-[75%] rounded-xl px-4 py-2.5 text-sm',
msg.is_internal
? 'bg-amber-50 border border-amber-200 text-amber-900'
: isOwnMessage(msg)
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-800'
]"
>
{{ messageForm.processing ? 'Sending…' : 'Send' }}
</button>
<div class="flex items-center gap-2 mb-1 text-xs opacity-70">
<span v-if="msg.is_internal">🔒 Internal Note · </span>
<span>{{ msg.author?.name || msg.author_email || 'Unknown' }}</span>
<span>· {{ timeAgo(msg.created_at) }}</span>
</div>
<p class="whitespace-pre-wrap">{{ msg.body }}</p>
</div>
</div>
</form>
</div>
<!-- Reply Area -->
<div class="border-t border-gray-200 p-5">
<!-- Tab switcher (agents only) -->
<div v-if="isAgent" class="flex gap-2 mb-3">
<button
@click="replyTab = 'reply'"
:class="[
'text-xs px-3 py-1.5 rounded-md font-medium transition',
replyTab === 'reply' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
>Reply to submitter</button>
<button
@click="replyTab = 'internal'"
:class="[
'text-xs px-3 py-1.5 rounded-md font-medium transition',
replyTab === 'internal' ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]"
>🔒 Internal Note</button>
</div>
<form @submit.prevent="sendMessage">
<textarea
v-model="messageForm.body"
required
rows="3"
:placeholder="replyTab === 'internal' ? 'Internal note — only visible to agents…' : 'Type your reply…'"
:class="[
'w-full rounded-lg text-sm border',
replyTab === 'internal'
? 'border-amber-300 bg-amber-50'
: 'border-gray-300'
]"
></textarea>
<div class="flex items-center justify-between mt-3">
<label class="flex items-center gap-2 text-xs text-gray-500 cursor-pointer">
<input type="file" class="hidden" @change="attachFile" />
<span class="text-gray-400 hover:text-indigo-600 transition">📎 Attach file</span>
</label>
<button
type="submit"
:disabled="messageForm.processing"
:class="[
'inline-flex items-center gap-1 text-sm font-medium px-4 py-2 rounded-lg transition',
replyTab === 'internal'
? 'bg-amber-500 text-white hover:bg-amber-600 disabled:opacity-60'
: 'bg-gray-900 text-white hover:bg-gray-700 disabled:opacity-60'
]"
>
{{ messageForm.processing ? 'Sending…' : 'Send' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
import { Link, useForm, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
const props = defineProps({
ticket: Object,
@@ -247,8 +247,6 @@ const messageForm = useForm({
})
function isOwnMessage(msg) {
// If the message user_id matches the current user (we don't have auth here easily,
// but for display purposes: agents on right when it's not submitter's message)
return !props.isAgent ? msg.user_id === props.ticket.submitter_id : msg.user_id !== props.ticket.submitter_id
}
@@ -295,11 +293,11 @@ function statusLabel(status) {
function statusClass(status) {
const map = {
open: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200',
in_progress: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-200',
pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200',
resolved: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200',
closed: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400',
open: 'bg-blue-100 text-blue-700',
in_progress: 'bg-purple-100 text-purple-700',
pending: 'bg-yellow-100 text-yellow-700',
resolved: 'bg-green-100 text-green-700',
closed: 'bg-gray-100 text-gray-500',
}
return map[status] || 'bg-gray-100 text-gray-600'
}