Implement parent child ticket hierarchy

This commit is contained in:
2026-03-25 08:35:17 +01:00
parent 1ff8f5d68f
commit 3b31e6bb7d
13 changed files with 617 additions and 46 deletions
+106 -39
View File
@@ -3,6 +3,7 @@ import { useForm, Head, Link } from '@inertiajs/vue3';
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import TicketTreeList from '@/components/TicketTreeList.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -57,6 +58,7 @@ const form = useForm({
value_user_id: currentFieldValue?.value_user_id ?? '',
};
}),
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
ticket_priority_id: props.ticket.ticket_priority_id,
ticket_type_id: props.ticket.ticket_type_id,
title: props.ticket.title,
@@ -220,6 +222,34 @@ const formatDateTime = (value: string): string => {
</div>
</div>
<div class="grid gap-2">
<Label for="parent_ticket_id">
Parent ticket
</Label>
<select
id="parent_ticket_id"
v-model="form.parent_ticket_id"
:disabled="
!canEditTicket || form.processing
"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-60"
>
<option value="">No parent ticket</option>
<option
v-for="parentTicketOption in parentTicketOptions"
:key="parentTicketOption.id"
:value="parentTicketOption.id"
>
{{
`${parentTicketOption.ticket_key} - ${parentTicketOption.title}`
}}
</option>
</select>
<InputError
:message="form.errors.parent_ticket_id"
/>
</div>
<div class="grid gap-2">
<Label for="description">Description</Label>
<textarea
@@ -357,53 +387,90 @@ const formatDateTime = (value: string): string => {
</CardContent>
</Card>
<Card class="h-fit">
<CardHeader>
<CardTitle>Worklog</CardTitle>
<CardDescription>
Every tracked ticket change is recorded with actor
and timestamp.
</CardDescription>
</CardHeader>
<div class="space-y-6">
<Card class="h-fit">
<CardHeader>
<CardTitle>Child tickets</CardTitle>
<CardDescription>
Parent tickets expose their full child tree
here, including nested parent chains.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div
v-if="(ticket.worklog_entries?.length ?? 0) === 0"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
No worklog entries exist for this ticket yet.
</div>
<div v-else class="flex flex-col gap-4">
<CardContent>
<div
v-for="worklogEntry in ticket.worklog_entries"
:key="worklogEntry.id"
class="rounded-xl border border-border px-4 py-4"
v-if="childTicketTree.length === 0"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
<div class="flex flex-wrap items-center gap-2">
<Badge variant="outline">
{{ worklogEntry.entry_type }}
</Badge>
<span
class="text-sm font-medium text-foreground"
No child tickets are assigned to this ticket
yet.
</div>
<TicketTreeList
v-else
:nodes="childTicketTree"
:project-id="project.id"
/>
</CardContent>
</Card>
<Card class="h-fit">
<CardHeader>
<CardTitle>Worklog</CardTitle>
<CardDescription>
Every tracked ticket change is recorded with
actor and timestamp.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div
v-if="
(ticket.worklog_entries?.length ?? 0) === 0
"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
No worklog entries exist for this ticket yet.
</div>
<div v-else class="flex flex-col gap-4">
<div
v-for="worklogEntry in ticket.worklog_entries"
:key="worklogEntry.id"
class="rounded-xl border border-border px-4 py-4"
>
<div
class="flex flex-wrap items-center gap-2"
>
<Badge variant="outline">
{{ worklogEntry.entry_type }}
</Badge>
<span
class="text-sm font-medium text-foreground"
>
{{
worklogEntry.actor?.name ??
'System'
}}
</span>
</div>
<p class="mt-3 text-sm text-foreground">
{{ worklogEntry.summary }}
</p>
<p
class="mt-2 text-xs text-muted-foreground"
>
{{
worklogEntry.actor?.name ?? 'System'
formatDateTime(
worklogEntry.created_at,
)
}}
</span>
</p>
</div>
<p class="mt-3 text-sm text-foreground">
{{ worklogEntry.summary }}
</p>
<p class="mt-2 text-xs text-muted-foreground">
{{
formatDateTime(worklogEntry.created_at)
}}
</p>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
</div>
</div>
</AppLayout>