Add ticketing project settings
This commit is contained in:
@@ -21,6 +21,10 @@
|
|||||||
{{ $page.props.errors.priority }}
|
{{ $page.props.errors.priority }}
|
||||||
</div>
|
</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">
|
||||||
|
{{ $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 dark:border-gray-700 mb-6">
|
||||||
<button
|
<button
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
@@ -258,6 +262,95 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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" />
|
||||||
|
</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>
|
||||||
|
</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">
|
||||||
|
Create Project
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-if="projects.length === 0" class="text-sm text-gray-400 italic">No projects defined yet.</div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">{{ 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>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button @click="startEditProject(project)" class="text-xs text-indigo-600 hover:underline">Edit</button>
|
||||||
|
<button @click="openDeleteProjectModal(project)" class="text-xs text-red-500 hover:underline">Delete</button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<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" />
|
||||||
|
</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">
|
||||||
|
<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" />
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
<button type="submit" :disabled="editProjectForm.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>
|
||||||
|
|
||||||
<div v-if="removeAgentModalOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
|
<div v-if="removeAgentModalOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
|
||||||
@@ -286,6 +379,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
|
Delete <strong>{{ pendingProjectDelete?.name }}</strong>? This cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div class="mt-6 flex justify-end gap-2">
|
||||||
|
<button type="button" @click="closeDeleteProjectModal" 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="confirmDeleteProject" class="bg-red-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-red-700">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -296,6 +402,7 @@ const props = defineProps({
|
|||||||
groups: Array,
|
groups: Array,
|
||||||
agents: Array,
|
agents: Array,
|
||||||
priorities: Array,
|
priorities: Array,
|
||||||
|
projects: Array,
|
||||||
myGroupIds: Array,
|
myGroupIds: Array,
|
||||||
isBootstrap: Boolean,
|
isBootstrap: Boolean,
|
||||||
isSiteAdmin: Boolean,
|
isSiteAdmin: Boolean,
|
||||||
@@ -306,23 +413,30 @@ const tabs = [
|
|||||||
{ key: 'groups', label: 'Groups' },
|
{ key: 'groups', label: 'Groups' },
|
||||||
{ key: 'agents', label: 'Agents' },
|
{ key: 'agents', label: 'Agents' },
|
||||||
{ key: 'priorities', label: 'Priorities' },
|
{ key: 'priorities', label: 'Priorities' },
|
||||||
|
{ key: 'projects', label: 'Projects' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const showAddGroup = ref(false)
|
const showAddGroup = ref(false)
|
||||||
const showAddAgent = ref(false)
|
const showAddAgent = ref(false)
|
||||||
const showAddPriority = ref(false)
|
const showAddPriority = ref(false)
|
||||||
|
const showAddProject = ref(false)
|
||||||
const editingGroup = ref(null)
|
const editingGroup = ref(null)
|
||||||
const editingPriority = ref(null)
|
const editingPriority = ref(null)
|
||||||
|
const editingProject = ref(null)
|
||||||
const removeAgentModalOpen = ref(false)
|
const removeAgentModalOpen = ref(false)
|
||||||
const pendingAgentRemoval = ref(null)
|
const pendingAgentRemoval = ref(null)
|
||||||
const deletePriorityModalOpen = ref(false)
|
const deletePriorityModalOpen = ref(false)
|
||||||
const pendingPriorityDelete = ref(null)
|
const pendingPriorityDelete = ref(null)
|
||||||
|
const deleteProjectModalOpen = ref(false)
|
||||||
|
const pendingProjectDelete = ref(null)
|
||||||
|
|
||||||
const groupForm = useForm({ name: '', email_address: '', color: '#6366f1', prefix: '' })
|
const groupForm = useForm({ name: '', email_address: '', color: '#6366f1', prefix: '' })
|
||||||
const editGroupForm = 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 agentForm = useForm({ user_id: '', group_id: '', role: 'agent' })
|
||||||
const priorityForm = useForm({ name: '', color: '#6b7280', description: '', sort_order: 0, group_id: null })
|
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 })
|
const editPriorityForm = useForm({ name: '', color: '#6b7280', description: '', sort_order: 0, group_id: null })
|
||||||
|
const projectForm = useForm({ group_id: '', name: '', description: '', status: 'active' })
|
||||||
|
const editProjectForm = useForm({ name: '', description: '', status: 'active' })
|
||||||
|
|
||||||
function submitGroup() {
|
function submitGroup() {
|
||||||
groupForm.post(route('ticketing.settings.groups.store'), {
|
groupForm.post(route('ticketing.settings.groups.store'), {
|
||||||
@@ -406,4 +520,41 @@ function confirmDeletePriority() {
|
|||||||
onFinish: closeDeletePriorityModal,
|
onFinish: closeDeletePriorityModal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitProject() {
|
||||||
|
projectForm.post(route('ticketing.settings.projects.store'), {
|
||||||
|
onSuccess: () => { showAddProject.value = false; projectForm.reset() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditProject(project) {
|
||||||
|
editingProject.value = project
|
||||||
|
editProjectForm.name = project.name
|
||||||
|
editProjectForm.description = project.description || ''
|
||||||
|
editProjectForm.status = project.status
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitEditProject() {
|
||||||
|
editProjectForm.put(route('ticketing.settings.projects.update', { project: editingProject.value.id }), {
|
||||||
|
onSuccess: () => { editingProject.value = null }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteProjectModal(project) {
|
||||||
|
pendingProjectDelete.value = project
|
||||||
|
deleteProjectModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteProjectModal() {
|
||||||
|
deleteProjectModalOpen.value = false
|
||||||
|
pendingProjectDelete.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteProject() {
|
||||||
|
if (!pendingProjectDelete.value) return
|
||||||
|
|
||||||
|
router.delete(route('ticketing.settings.projects.destroy', { project: pendingProjectDelete.value.id }), {
|
||||||
|
onFinish: closeDeleteProjectModal,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Dashboard\Ticketing\Http\Controllers;
|
|||||||
use Dashboard\Ticketing\Models\PriorityLevel;
|
use Dashboard\Ticketing\Models\PriorityLevel;
|
||||||
use Dashboard\Ticketing\Models\TicketingAgentAccess;
|
use Dashboard\Ticketing\Models\TicketingAgentAccess;
|
||||||
use Dashboard\Ticketing\Models\TicketingGroup;
|
use Dashboard\Ticketing\Models\TicketingGroup;
|
||||||
|
use Dashboard\Ticketing\Models\TicketingProject;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller;
|
use Illuminate\Routing\Controller;
|
||||||
@@ -87,10 +88,17 @@ class TicketingSettingsController extends Controller
|
|||||||
: PriorityLevel::where(fn($q) => $q->whereNull('group_id')->orWhereIn('group_id', $myGroupIds))
|
: PriorityLevel::where(fn($q) => $q->whereNull('group_id')->orWhereIn('group_id', $myGroupIds))
|
||||||
->orderBy('sort_order')->get();
|
->orderBy('sort_order')->get();
|
||||||
|
|
||||||
|
$projects = $isBootstrap
|
||||||
|
? collect()
|
||||||
|
: TicketingProject::whereIn('group_id', $myGroupIds)
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
return Inertia::render('Ticketing/Settings', [
|
return Inertia::render('Ticketing/Settings', [
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
'agents' => $agents,
|
'agents' => $agents,
|
||||||
'priorities' => $priorities,
|
'priorities' => $priorities,
|
||||||
|
'projects' => $projects,
|
||||||
'myGroupIds' => $myGroupIds,
|
'myGroupIds' => $myGroupIds,
|
||||||
'isBootstrap' => $isBootstrap,
|
'isBootstrap' => $isBootstrap,
|
||||||
'isSiteAdmin' => $this->isSiteAdmin(),
|
'isSiteAdmin' => $this->isSiteAdmin(),
|
||||||
@@ -261,4 +269,57 @@ class TicketingSettingsController extends Controller
|
|||||||
|
|
||||||
return back()->with('success', 'Priority level removed.');
|
return back()->with('success', 'Priority level removed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function storeProject(Request $request)
|
||||||
|
{
|
||||||
|
$this->requireAgentAccess();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'group_id' => 'required|exists:ticketing_groups,id',
|
||||||
|
'name' => 'required|string|max:100',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'status' => 'required|in:active,archived',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->requireManagerAccess($validated['group_id']);
|
||||||
|
|
||||||
|
TicketingProject::create([
|
||||||
|
...$validated,
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Project created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProject(Request $request, TicketingProject $project)
|
||||||
|
{
|
||||||
|
$this->requireAgentAccess();
|
||||||
|
$this->requireManagerAccess($project->group_id);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:100',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'status' => 'required|in:active,archived',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$project->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', 'Project updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyProject(TicketingProject $project)
|
||||||
|
{
|
||||||
|
$this->requireAgentAccess();
|
||||||
|
$this->requireManagerAccess($project->group_id);
|
||||||
|
|
||||||
|
if ($project->tickets()->exists()) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'project' => 'Cannot delete a project that is in use by tickets.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$project->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Project removed.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ Route::middleware(['web', 'auth', 'app.access:ticketing'])->prefix('app/ticketin
|
|||||||
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority'])->name('settings.priorities.store');
|
Route::post('/settings/priorities', [TicketingSettingsController::class, 'storePriority'])->name('settings.priorities.store');
|
||||||
Route::put('/settings/priorities/{priority}', [TicketingSettingsController::class, 'updatePriority'])->name('settings.priorities.update');
|
Route::put('/settings/priorities/{priority}', [TicketingSettingsController::class, 'updatePriority'])->name('settings.priorities.update');
|
||||||
Route::delete('/settings/priorities/{priority}', [TicketingSettingsController::class, 'destroyPriority'])->name('settings.priorities.destroy');
|
Route::delete('/settings/priorities/{priority}', [TicketingSettingsController::class, 'destroyPriority'])->name('settings.priorities.destroy');
|
||||||
|
Route::post('/settings/projects', [TicketingSettingsController::class, 'storeProject'])->name('settings.projects.store');
|
||||||
|
Route::put('/settings/projects/{project}', [TicketingSettingsController::class, 'updateProject'])->name('settings.projects.update');
|
||||||
|
Route::delete('/settings/projects/{project}', [TicketingSettingsController::class, 'destroyProject'])->name('settings.projects.destroy');
|
||||||
|
|
||||||
// Ticket routes
|
// Ticket routes
|
||||||
Route::get('/', [TicketController::class, 'index'])->name('index');
|
Route::get('/', [TicketController::class, 'index'])->name('index');
|
||||||
|
|||||||
Reference in New Issue
Block a user