/** * "Tickets" tab of the web workspace (ticket #86). * * A mobile-first adaptation of the desktop `TicketsPanel`/`TicketDetail` pair: * one vertical column that swaps between **list**, **create**, and **detail** * modes in place — never a floating window / dock, per carnet #86. Reuses the * transport-neutral `useTickets` view-model as-is; only the presentation (and * the French labels — decision #78) is web-specific. */ import { useState } from "react"; import type { Sprint, TicketSummary } from "@/domain"; import type { TicketListSortField } from "@/ports"; import { TICKET_PRIORITIES, TICKET_STATUSES, useTickets } from "@/features/tickets"; import { Button, Input, Panel, Spinner, cn } from "@/shared"; import { WebTicketCreate } from "./WebTicketCreate"; import { WebTicketDetail } from "./WebTicketDetail"; import { useWebProjectAgents } from "./useWebProjectAgents"; import { WebStatusBadge, WebPriorityBadge, webStatusLabel, webPriorityLabel } from "./webTicketLabels"; const selectClass = cn( "h-8 rounded-md bg-raised px-2 text-xs text-content", "border border-border outline-none transition-colors", "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50", ); const SORT_FIELD_LABEL: Record = { number: "Numéro", priority: "Priorité", status: "Statut", title: "Titre", }; type Mode = { type: "list" } | { type: "create" } | { type: "detail"; ref: string }; export interface WebTicketsViewProps { projectId: string; /** * When set, the list initially shows only this sprint's tickets (from the * Sprints tab's "Voir tickets"), with a link back to the full list. */ focusSprintId?: string | null; } export function WebTicketsView({ projectId, focusSprintId = null }: WebTicketsViewProps) { const vm = useTickets(projectId); const webAgents = useWebProjectAgents(projectId); const [mode, setMode] = useState({ type: "list" }); const [sprintFocus, setSprintFocus] = useState(focusSprintId); if (mode.type === "detail") { return ( setMode({ type: "list" })} onOpenRef={(ref) => setMode({ type: "detail", ref })} /> ); } if (mode.type === "create") { return ( setMode({ type: "list" })} onCreate={async (input, sprintId) => { const created = await vm.create(input); if (!created) return; if (sprintId) await vm.assignSprint(created.ref, sprintId); setMode({ type: "detail", ref: created.ref }); }} /> ); } const items = vm.list?.items ?? []; const hasFilters = !!vm.query.text || (vm.query.statuses?.length ?? 0) > 0 || (vm.query.priorities?.length ?? 0) > 0 || !!vm.query.assignedAgentId || !!sprintFocus; const sprintIds = new Set(vm.sprints.map((s) => s.id)); const visibleSprints = sprintFocus ? vm.sprints.filter((s) => s.id === sprintFocus) : vm.sprints; const grouped = visibleSprints .map((sprint) => ({ sprint, tickets: items.filter((t) => t.sprintId === sprint.id) })) .filter((g) => g.tickets.length > 0); const noSprint = sprintFocus ? [] : items.filter((t) => !t.sprintId || !sprintIds.has(t.sprintId)); function resetFilters() { vm.setQuery({}); setSprintFocus(null); } return ( setMode({ type: "create" })}> + Ticket } > {vm.error && (
{vm.error}
)} {sprintFocus && (
Filtré sur le sprint « {vm.sprints.find((s) => s.id === sprintFocus)?.name ?? sprintFocus} »
)} {/* ── Search + compact filters ── */}
vm.setQuery({ ...vm.query, text: e.target.value || undefined })} />
{/* ── List, grouped by sprint ── */} {vm.busy && vm.list === null ? ( Chargement des tickets… ) : items.length === 0 && !hasFilters ? (

Aucun ticket dans ce projet.

) : items.length === 0 ? (

Aucun ticket ne correspond aux filtres.

) : (
{grouped.map((g) => ( setMode({ type: "detail", ref })} /> ))} {noSprint.length > 0 && ( setMode({ type: "detail", ref })} /> )}
)} {vm.list?.nextCursor && (
)}
); } function TicketGroup({ heading, tickets, sprints, nameOf, onOpen, }: { heading: string; tickets: TicketSummary[]; sprints: Sprint[]; nameOf: (id: string) => string; onOpen: (ref: string) => void; }) { const sprintNameOf = (id: string | null | undefined) => id ? sprints.find((s) => s.id === id)?.name : undefined; return (

{heading} {tickets.length}

    {tickets.map((t) => { const sprintName = sprintNameOf(t.sprintId); const assignees = t.assignedAgentIds.map(nameOf).join(", "); return (
  • ); })}
); }