Implement ticket creation and backlog

This commit is contained in:
2026-03-25 01:33:35 +01:00
parent 35aacff318
commit f7a0543ea1
17 changed files with 873 additions and 3 deletions
+265
View File
@@ -0,0 +1,265 @@
<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import ProjectBacklogController from '@/actions/App/Http/Controllers/ProjectBacklogController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import AppLayout from '@/layouts/AppLayout.vue';
import { index as backlogIndex } from '@/routes/projects/backlog';
import { index, show } from '@/routes/projects';
import { metadata as metadataSettings } from '@/routes/projects/settings';
import type { BreadcrumbItem, ProjectBacklogPageProps } from '@/types';
const props = defineProps<ProjectBacklogPageProps>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Projects',
href: index(),
},
{
title: props.project.name,
href: show(props.project.id),
},
{
title: 'Backlog',
href: backlogIndex(props.project.id),
},
];
const hasTicketMetadata =
props.ticketTypes.length > 0 && props.ticketPriorities.length > 0;
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head :title="`${project.name} backlog`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
<div class="space-y-3">
<Badge variant="outline" class="px-3 py-1">
{{ project.key }}
</Badge>
<Heading
:title="`${project.name} backlog`"
description="All newly created tickets start here before they move onto the board."
/>
</div>
<Button as-child variant="outline">
<Link :href="show(project.id)">Back to project</Link>
</Button>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
{{ status }}
</div>
<Card v-if="canCreateTickets && hasTicketMetadata">
<CardHeader>
<CardTitle>Create ticket</CardTitle>
<CardDescription>
New tickets receive the next immutable project key and
automatically enter backlog.
</CardDescription>
</CardHeader>
<CardContent>
<Form
v-bind="ProjectBacklogController.store.form(project.id)"
class="grid gap-6"
v-slot="{ errors, processing }"
>
<div class="grid gap-2">
<Label for="title">Title</Label>
<Input
id="title"
name="title"
required
autofocus
placeholder="Implement repository sync"
/>
<InputError :message="errors.title" />
</div>
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
<div class="grid gap-2">
<Label for="ticket_type_id">Type</Label>
<select
id="ticket_type_id"
name="ticket_type_id"
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"
>
<option
v-for="ticketType in ticketTypes"
:key="ticketType.id"
:value="ticketType.id"
>
{{ ticketType.name }}
</option>
</select>
<InputError :message="errors.ticket_type_id" />
</div>
<div class="grid gap-2">
<Label for="ticket_priority_id">
Priority
</Label>
<select
id="ticket_priority_id"
name="ticket_priority_id"
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"
>
<option
v-for="ticketPriority in ticketPriorities"
:key="ticketPriority.id"
:value="ticketPriority.id"
>
{{ ticketPriority.name }}
</option>
</select>
<InputError
:message="errors.ticket_priority_id"
/>
</div>
</div>
<div class="grid gap-2">
<Label for="description">Description</Label>
<textarea
id="description"
name="description"
rows="4"
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"
placeholder="Describe the expected behavior and relevant context."
/>
<InputError :message="errors.description" />
</div>
<div class="grid gap-2">
<Label for="acceptance_criteria">
Acceptance criteria
</Label>
<textarea
id="acceptance_criteria"
name="acceptance_criteria"
rows="4"
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"
placeholder="List the conditions that must be true before this ticket is done."
/>
<InputError :message="errors.acceptance_criteria" />
</div>
<div class="flex items-center gap-3">
<Button type="submit" :disabled="processing">
Create ticket
</Button>
</div>
</Form>
</CardContent>
</Card>
<Card v-else-if="canCreateTickets">
<CardContent class="flex flex-col gap-4 p-6 text-sm">
<p class="text-muted-foreground">
Ticket creation needs at least one type and one
priority.
</p>
<div>
<Button as-child variant="outline">
<Link :href="metadataSettings(project.id)">
Open ticket settings
</Link>
</Button>
</div>
</CardContent>
</Card>
<Card class="gap-0 overflow-hidden">
<CardHeader class="border-b">
<CardTitle>Backlog tickets</CardTitle>
<CardDescription>
Tickets remain here until they are moved into a board
column.
</CardDescription>
</CardHeader>
<CardContent class="p-0">
<div
v-if="tickets.length === 0"
class="p-6 text-sm text-muted-foreground"
>
No backlog tickets exist yet.
</div>
<div v-else class="overflow-x-auto">
<table
class="min-w-full divide-y divide-border text-sm"
>
<thead
class="bg-muted/40 text-left text-muted-foreground"
>
<tr>
<th class="px-4 py-3 font-medium">Key</th>
<th class="px-4 py-3 font-medium">Title</th>
<th class="px-4 py-3 font-medium">Type</th>
<th class="px-4 py-3 font-medium">
Priority
</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr
v-for="ticket in tickets"
:key="ticket.id"
class="bg-background"
>
<td class="px-4 py-4">
<Badge variant="outline">
{{ ticket.ticket_key }}
</Badge>
</td>
<td class="px-4 py-4">
<div
class="font-medium text-foreground"
>
{{ ticket.title }}
</div>
<div
v-if="ticket.description"
class="mt-1 line-clamp-2 text-muted-foreground"
>
{{ ticket.description }}
</div>
</td>
<td class="px-4 py-4 text-muted-foreground">
{{ ticket.ticket_type.name }}
</td>
<td class="px-4 py-4 text-muted-foreground">
{{ ticket.ticket_priority.name }}
</td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
</template>
+12 -3
View File
@@ -13,6 +13,7 @@ import {
} from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { access, index, show } from '@/routes/projects';
import { index as backlogIndex } from '@/routes/projects/backlog';
import {
board as boardSettings,
metadata as metadataSettings,
@@ -72,9 +73,17 @@ const projectAreas = [
/>
</div>
<Button as-child variant="outline">
<Link :href="index()">Back to projects</Link>
</Button>
<div class="flex flex-wrap gap-3">
<Button as-child variant="outline">
<Link :href="backlogIndex(project.id)">
Open backlog
</Link>
</Button>
<Button as-child variant="outline">
<Link :href="index()">Back to projects</Link>
</Button>
</div>
</div>
<div v-if="canManageAccess" class="flex flex-wrap gap-3">
+20
View File
@@ -0,0 +1,20 @@
import type { TicketPriorityModel } from '@/types/models/ticket-priority-model';
import type { TicketTypeModel } from '@/types/models/ticket-type-model';
export type TicketModel = {
id: number;
acceptance_criteria: string | null;
board_column_id: number | null;
description: string | null;
project_id: number;
sequence_number: number;
ticket_key: string;
ticket_priority: TicketPriorityModel;
ticket_priority_id: number;
ticket_type: TicketTypeModel;
ticket_type_id: number;
title: string;
created_at: string;
updated_at: string;
[key: string]: unknown;
};
+10
View File
@@ -2,6 +2,7 @@ import type { BoardColumnModel } from '@/types/models/board-column-model';
import type { ProjectModel } from '@/types/models/project-model';
import type { TeamModel } from '@/types/models/team-model';
import type { TicketFieldDefinitionModel } from '@/types/models/ticket-field-definition-model';
import type { TicketModel } from '@/types/models/ticket-model';
import type { TicketPriorityModel } from '@/types/models/ticket-priority-model';
import type { TicketTypeModel } from '@/types/models/ticket-type-model';
import type { UserModel } from '@/types/models/user-model';
@@ -60,3 +61,12 @@ export type ProjectTicketMetadataSettingsPageProps = {
ticketPriorities: TicketPriorityModel[];
ticketTypes: TicketTypeModel[];
};
export type ProjectBacklogPageProps = {
canCreateTickets: boolean;
project: ProjectModel;
status?: string | null;
ticketPriorities: TicketPriorityModel[];
tickets: TicketModel[];
ticketTypes: TicketTypeModel[];
};