From 7eac03f6c2d20b608e8aba33fe1182e592dc5fc2 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Sun, 29 Mar 2026 10:27:39 -0500 Subject: [PATCH] Add repair notes journal with running feed, visibility, and status tagging New repair_note table for timestamped journal entries on tickets. Each note captures author, content, visibility (internal or customer-facing), and the ticket status at time of writing. Notes display as a running feed on the ticket detail page with newest first. Internal notes have a lock icon, customer-visible notes highlighted in blue. Supports add and delete with appropriate permission gating. --- packages/admin/src/api/repairs.ts | 23 ++- .../src/components/repairs/ticket-notes.tsx | 167 ++++++++++++++++++ .../_authenticated/repairs/$ticketId.tsx | 11 ++ packages/admin/src/types/repair.ts | 11 ++ .../src/db/migrations/0018_repair_notes.sql | 12 ++ .../src/db/migrations/meta/_journal.json | 7 + packages/backend/src/db/schema/repairs.ts | 20 +++ packages/backend/src/routes/v1/repairs.ts | 36 +++- .../backend/src/services/repair.service.ts | 35 ++++ packages/shared/src/schemas/index.ts | 3 + packages/shared/src/schemas/repairs.schema.ts | 11 ++ 11 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 packages/admin/src/components/repairs/ticket-notes.tsx create mode 100644 packages/backend/src/db/migrations/0018_repair_notes.sql diff --git a/packages/admin/src/api/repairs.ts b/packages/admin/src/api/repairs.ts index a2d9b71..7a5329a 100644 --- a/packages/admin/src/api/repairs.ts +++ b/packages/admin/src/api/repairs.ts @@ -1,6 +1,6 @@ import { queryOptions } from '@tanstack/react-query' import { api } from '@/lib/api-client' -import type { RepairTicket, RepairLineItem, RepairBatch, RepairServiceTemplate } from '@/types/repair' +import type { RepairTicket, RepairLineItem, RepairBatch, RepairNote, RepairServiceTemplate } from '@/types/repair' import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas' // --- Repair Tickets --- @@ -89,6 +89,27 @@ export function repairBatchTicketsOptions(batchId: string, params: PaginationInp }) } +// --- Repair Notes --- + +export const repairNoteKeys = { + all: (ticketId: string) => ['repair-tickets', ticketId, 'notes'] as const, +} + +export function repairNoteListOptions(ticketId: string) { + return queryOptions({ + queryKey: repairNoteKeys.all(ticketId), + queryFn: () => api.get<{ data: RepairNote[] }>(`/v1/repair-tickets/${ticketId}/notes`), + enabled: !!ticketId, + }) +} + +export const repairNoteMutations = { + create: (ticketId: string, data: Record) => + api.post(`/v1/repair-tickets/${ticketId}/notes`, data), + delete: (id: string) => + api.del(`/v1/repair-notes/${id}`), +} + // --- Repair Service Templates --- export const repairServiceTemplateKeys = { diff --git a/packages/admin/src/components/repairs/ticket-notes.tsx b/packages/admin/src/components/repairs/ticket-notes.tsx new file mode 100644 index 0000000..db80e19 --- /dev/null +++ b/packages/admin/src/components/repairs/ticket-notes.tsx @@ -0,0 +1,167 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { repairNoteListOptions, repairNoteMutations, repairNoteKeys } from '@/api/repairs' +import { useAuthStore } from '@/stores/auth.store' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { Badge } from '@/components/ui/badge' +import { Send, Trash2, Eye, Lock } from 'lucide-react' +import { toast } from 'sonner' +import type { RepairNote } from '@/types/repair' + +const STATUS_LABELS: Record = { + in_transit: 'In Transit', + intake: 'Intake', + diagnosing: 'Diagnosing', + pending_approval: 'Pending Approval', + approved: 'Approved', + in_progress: 'In Progress', + pending_parts: 'Pending Parts', + ready: 'Ready', + picked_up: 'Picked Up', + delivered: 'Delivered', + cancelled: 'Cancelled', +} + +interface TicketNotesProps { + ticketId: string +} + +export function TicketNotes({ ticketId }: TicketNotesProps) { + const queryClient = useQueryClient() + const hasPermission = useAuthStore((s) => s.hasPermission) + const [content, setContent] = useState('') + const [visibility, setVisibility] = useState<'internal' | 'customer'>('internal') + + const { data } = useQuery(repairNoteListOptions(ticketId)) + const notes = data?.data ?? [] + + const createMutation = useMutation({ + mutationFn: (data: Record) => repairNoteMutations.create(ticketId, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: repairNoteKeys.all(ticketId) }) + setContent('') + toast.success('Note added') + }, + onError: (err) => toast.error(err.message), + }) + + const deleteMutation = useMutation({ + mutationFn: repairNoteMutations.delete, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: repairNoteKeys.all(ticketId) }) + toast.success('Note removed') + }, + onError: (err) => toast.error(err.message), + }) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!content.trim()) return + createMutation.mutate({ content: content.trim(), visibility }) + } + + function formatDate(dateStr: string) { + const d = new Date(dateStr) + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + + ' ' + d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) + } + + return ( +
+ {/* Add note form */} + {hasPermission('repairs.edit') && ( +
+