Files
KanbanGit/resources/js/pages/projects/Backlog.vue
T
2026-04-13 00:10:09 +02:00

369 lines
18 KiB
Vue

<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import ProjectBacklogController from '@/actions/App/Http/Controllers/ProjectBacklogController';
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
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 boardIndex } from '@/routes/projects/board';
import { index as backlogIndex } from '@/routes/projects/backlog';
import { index, show } from '@/routes/projects/index';
import { show as showTicket } from '@/routes/projects/tickets';
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>
<div class="flex flex-wrap gap-3">
<Button as-child variant="outline">
<Link :href="boardIndex(project.id)">Open board</Link>
</Button>
<Button as-child variant="outline">
<Link :href="show(project.id)">Back to project</Link>
</Button>
</div>
</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="flex gap-2">
<div class="flex-4/5 grid gap-2 md:gap-6">
<div class="grid gap-2">
<Label for="description">Description</Label>
<textarea
id="description"
name="description"
rows="4"
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
placeholder="List the conditions that must be true before this ticket is done."
/>
<InputError
:message="errors.acceptance_criteria"
/>
</div>
</div>
<div
class="grid h-fit flex-1 gap-2 md:ml-4 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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
>
<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="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
>
<option
v-for="ticketPriority in ticketPriorities"
:key="ticketPriority.id"
:value="ticketPriority.id"
>
{{ ticketPriority.name }}
</option>
</select>
<InputError
:message="errors.ticket_priority_id"
/>
</div>
<div
class="grid gap-2"
v-if="project.show_story_points"
>
<Label for="story_points">
Estimated story points
</Label>
<Input
id="story_points"
name="story_points"
type="number"
step="1"
/>
<InputError
:message="errors.story_points"
/>
</div>
</div>
</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="text-muted-foreground p-6 text-sm"
>
No backlog tickets exist yet.
</div>
<div v-else class="overflow-x-auto">
<table
class="divide-border min-w-full divide-y text-sm"
>
<thead
class="bg-muted/40 text-muted-foreground text-left"
>
<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>
<th
v-if="canEditTickets"
class="px-4 py-3 font-medium"
>
Move to board
</th>
</tr>
</thead>
<tbody class="divide-border divide-y">
<tr
v-for="ticket in tickets"
:key="ticket.id"
class="bg-background"
>
<td class="px-4 py-4">
<Link
:href="
showTicket({
project: project.id,
ticket: ticket.id,
})
"
>
<Badge variant="outline">
{{ ticket.ticket_key }}
</Badge>
</Link>
</td>
<td class="px-4 py-4">
<Link
:href="
showTicket({
project: project.id,
ticket: ticket.id,
})
"
class="text-foreground hover:text-primary font-medium transition"
>
{{ ticket.title }}
</Link>
<div
v-if="ticket.description"
class="text-muted-foreground mt-1 line-clamp-2"
>
{{ ticket.description }}
</div>
</td>
<td class="text-muted-foreground px-4 py-4">
{{ ticket.ticket_type.name }}
</td>
<td class="text-muted-foreground px-4 py-4">
{{ ticket.ticket_priority.name }}
</td>
<td v-if="canEditTickets" class="px-4 py-4">
<Form
v-if="boardColumns.length > 0"
v-bind="
ProjectBoardController.move.form(
{
project: project.id,
ticket: ticket.id,
},
)
"
class="flex flex-col gap-2 sm:flex-row"
v-slot="{ processing }"
>
<input
type="hidden"
name="redirect_to"
value="backlog"
/>
<select
name="board_column_id"
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-w-44 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
>
<option
v-for="boardColumn in boardColumns"
:key="boardColumn.id"
:value="boardColumn.id"
>
{{ boardColumn.name }}
</option>
</select>
<Button
type="submit"
size="sm"
:disabled="processing"
>
Move
</Button>
</Form>
<span
v-else
class="text-muted-foreground text-sm"
>
Add board columns first.
</span>
</td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
</template>