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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query'
|
import { queryOptions } from '@tanstack/react-query'
|
||||||
import { api } from '@/lib/api-client'
|
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'
|
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||||
|
|
||||||
// --- Repair Tickets ---
|
// --- 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<string, unknown>) =>
|
||||||
|
api.post<RepairNote>(`/v1/repair-tickets/${ticketId}/notes`, data),
|
||||||
|
delete: (id: string) =>
|
||||||
|
api.del<RepairNote>(`/v1/repair-notes/${id}`),
|
||||||
|
}
|
||||||
|
|
||||||
// --- Repair Service Templates ---
|
// --- Repair Service Templates ---
|
||||||
|
|
||||||
export const repairServiceTemplateKeys = {
|
export const repairServiceTemplateKeys = {
|
||||||
|
|||||||
167
packages/admin/src/components/repairs/ticket-notes.tsx
Normal file
167
packages/admin/src/components/repairs/ticket-notes.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
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<string, unknown>) => 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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Add note form */}
|
||||||
|
{hasPermission('repairs.edit') && (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-2">
|
||||||
|
<Textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="Add a note..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVisibility('internal')}
|
||||||
|
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border transition-colors ${
|
||||||
|
visibility === 'internal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Lock className="h-3 w-3" />Internal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVisibility('customer')}
|
||||||
|
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border transition-colors ${
|
||||||
|
visibility === 'customer' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />Customer Visible
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" size="sm" disabled={createMutation.isPending || !content.trim()}>
|
||||||
|
<Send className="mr-1 h-3 w-3" />
|
||||||
|
{createMutation.isPending ? 'Posting...' : 'Post Note'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes feed */}
|
||||||
|
{notes.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">No notes yet</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[...notes].reverse().map((note) => (
|
||||||
|
<NoteEntry
|
||||||
|
key={note.id}
|
||||||
|
note={note}
|
||||||
|
formatDate={formatDate}
|
||||||
|
canDelete={hasPermission('repairs.admin')}
|
||||||
|
onDelete={() => deleteMutation.mutate(note.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoteEntry({ note, formatDate, canDelete, onDelete }: {
|
||||||
|
note: RepairNote
|
||||||
|
formatDate: (d: string) => string
|
||||||
|
canDelete: boolean
|
||||||
|
onDelete: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-md border p-3 ${note.visibility === 'customer' ? 'border-blue-300/50 bg-blue-50/30 dark:border-blue-800/50 dark:bg-blue-950/20' : ''}`}>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="font-semibold text-foreground">{note.authorName}</span>
|
||||||
|
<span>{formatDate(note.createdAt)}</span>
|
||||||
|
{note.ticketStatus && (
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||||
|
{STATUS_LABELS[note.ticketStatus] ?? note.ticketStatus}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.visibility === 'customer' && (
|
||||||
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 gap-0.5">
|
||||||
|
<Eye className="h-2.5 w-2.5" />Customer
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.visibility === 'internal' && (
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0 gap-0.5">
|
||||||
|
<Lock className="h-2.5 w-2.5" />Internal
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{canDelete && (
|
||||||
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={onDelete}>
|
||||||
|
<Trash2 className="h-3 w-3 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm mt-1.5 whitespace-pre-wrap">{note.content}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { usePagination } from '@/hooks/use-pagination'
|
import { usePagination } from '@/hooks/use-pagination'
|
||||||
import { StatusProgress } from '@/components/repairs/status-progress'
|
import { StatusProgress } from '@/components/repairs/status-progress'
|
||||||
import { TicketPhotos } from '@/components/repairs/ticket-photos'
|
import { TicketPhotos } from '@/components/repairs/ticket-photos'
|
||||||
|
import { TicketNotes } from '@/components/repairs/ticket-notes'
|
||||||
import { DataTable, type Column } from '@/components/shared/data-table'
|
import { DataTable, type Column } from '@/components/shared/data-table'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -334,6 +335,16 @@ function RepairTicketDetailPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Notes Journal */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Notes</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TicketNotes ticketId={ticketId} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Line Items */}
|
{/* Line Items */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ export interface RepairBatch {
|
|||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RepairNote {
|
||||||
|
id: string
|
||||||
|
repairTicketId: string
|
||||||
|
authorId: string
|
||||||
|
authorName: string
|
||||||
|
content: string
|
||||||
|
visibility: 'internal' | 'customer'
|
||||||
|
ticketStatus: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface RepairServiceTemplate {
|
export interface RepairServiceTemplate {
|
||||||
id: string
|
id: string
|
||||||
companyId: string
|
companyId: string
|
||||||
|
|||||||
12
packages/backend/src/db/migrations/0018_repair_notes.sql
Normal file
12
packages/backend/src/db/migrations/0018_repair_notes.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TYPE "repair_note_visibility" AS ENUM ('internal', 'customer');
|
||||||
|
|
||||||
|
CREATE TABLE "repair_note" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"repair_ticket_id" uuid NOT NULL REFERENCES "repair_ticket"("id"),
|
||||||
|
"author_id" uuid NOT NULL REFERENCES "user"("id"),
|
||||||
|
"author_name" varchar(255) NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"visibility" "repair_note_visibility" NOT NULL DEFAULT 'internal',
|
||||||
|
"ticket_status" "repair_ticket_status",
|
||||||
|
"created_at" timestamp with time zone NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
@@ -127,6 +127,13 @@
|
|||||||
"when": 1774770000000,
|
"when": 1774770000000,
|
||||||
"tag": "0017_repair_in_transit_status",
|
"tag": "0017_repair_in_transit_status",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774780000000,
|
||||||
|
"tag": "0018_repair_notes",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -140,6 +140,26 @@ export const repairLineItems = pgTable('repair_line_item', {
|
|||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const repairNoteVisibilityEnum = pgEnum('repair_note_visibility', ['internal', 'customer'])
|
||||||
|
|
||||||
|
export const repairNotes = pgTable('repair_note', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
repairTicketId: uuid('repair_ticket_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => repairTickets.id),
|
||||||
|
authorId: uuid('author_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
authorName: varchar('author_name', { length: 255 }).notNull(),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
visibility: repairNoteVisibilityEnum('visibility').notNull().default('internal'),
|
||||||
|
ticketStatus: repairTicketStatusEnum('ticket_status'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RepairNote = typeof repairNotes.$inferSelect
|
||||||
|
export type RepairNoteInsert = typeof repairNotes.$inferInsert
|
||||||
|
|
||||||
export const repairServiceTemplates = pgTable('repair_service_template', {
|
export const repairServiceTemplates = pgTable('repair_service_template', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
companyId: uuid('company_id')
|
companyId: uuid('company_id')
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import {
|
|||||||
RepairBatchCreateSchema,
|
RepairBatchCreateSchema,
|
||||||
RepairBatchUpdateSchema,
|
RepairBatchUpdateSchema,
|
||||||
RepairBatchStatusUpdateSchema,
|
RepairBatchStatusUpdateSchema,
|
||||||
|
RepairNoteCreateSchema,
|
||||||
RepairServiceTemplateCreateSchema,
|
RepairServiceTemplateCreateSchema,
|
||||||
RepairServiceTemplateUpdateSchema,
|
RepairServiceTemplateUpdateSchema,
|
||||||
} from '@forte/shared/schemas'
|
} from '@forte/shared/schemas'
|
||||||
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairServiceTemplateService } from '../../services/repair.service.js'
|
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
|
||||||
|
|
||||||
export const repairRoutes: FastifyPluginAsync = async (app) => {
|
export const repairRoutes: FastifyPluginAsync = async (app) => {
|
||||||
// --- Repair Tickets ---
|
// --- Repair Tickets ---
|
||||||
@@ -187,6 +188,39 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return reply.send(result)
|
return reply.send(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Repair Notes ---
|
||||||
|
|
||||||
|
app.post('/repair-tickets/:ticketId/notes', { preHandler: [app.authenticate, app.requirePermission('repairs.edit')] }, async (request, reply) => {
|
||||||
|
const { ticketId } = request.params as { ticketId: string }
|
||||||
|
const parsed = RepairNoteCreateSchema.safeParse(request.body)
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||||
|
}
|
||||||
|
const ticket = await RepairTicketService.getById(app.db, request.companyId, ticketId)
|
||||||
|
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
|
||||||
|
|
||||||
|
// Look up author name from users table
|
||||||
|
const { users } = await import('../../db/schema/users.js')
|
||||||
|
const { eq } = await import('drizzle-orm')
|
||||||
|
const [author] = await app.db.select({ firstName: users.firstName, lastName: users.lastName }).from(users).where(eq(users.id, request.user.id)).limit(1)
|
||||||
|
const authorName = author ? `${author.firstName} ${author.lastName}` : 'Unknown'
|
||||||
|
const note = await RepairNoteService.create(app.db, ticketId, request.user.id, authorName, ticket.status, parsed.data)
|
||||||
|
return reply.status(201).send(note)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/repair-tickets/:ticketId/notes', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
|
||||||
|
const { ticketId } = request.params as { ticketId: string }
|
||||||
|
const notes = await RepairNoteService.listByTicket(app.db, ticketId)
|
||||||
|
return reply.send({ data: notes })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/repair-notes/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string }
|
||||||
|
const note = await RepairNoteService.delete(app.db, id)
|
||||||
|
if (!note) return reply.status(404).send({ error: { message: 'Note not found', statusCode: 404 } })
|
||||||
|
return reply.send(note)
|
||||||
|
})
|
||||||
|
|
||||||
// --- Repair Service Templates ---
|
// --- Repair Service Templates ---
|
||||||
|
|
||||||
app.post('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
app.post('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
repairLineItems,
|
repairLineItems,
|
||||||
repairBatches,
|
repairBatches,
|
||||||
repairServiceTemplates,
|
repairServiceTemplates,
|
||||||
|
repairNotes,
|
||||||
} from '../db/schema/repairs.js'
|
} from '../db/schema/repairs.js'
|
||||||
import type {
|
import type {
|
||||||
RepairTicketCreateInput,
|
RepairTicketCreateInput,
|
||||||
@@ -13,6 +14,7 @@ import type {
|
|||||||
RepairLineItemUpdateInput,
|
RepairLineItemUpdateInput,
|
||||||
RepairBatchCreateInput,
|
RepairBatchCreateInput,
|
||||||
RepairBatchUpdateInput,
|
RepairBatchUpdateInput,
|
||||||
|
RepairNoteCreateInput,
|
||||||
RepairServiceTemplateCreateInput,
|
RepairServiceTemplateCreateInput,
|
||||||
RepairServiceTemplateUpdateInput,
|
RepairServiceTemplateUpdateInput,
|
||||||
PaginationInput,
|
PaginationInput,
|
||||||
@@ -457,3 +459,36 @@ export const RepairServiceTemplateService = {
|
|||||||
return template ?? null
|
return template ?? null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RepairNoteService = {
|
||||||
|
async create(db: PostgresJsDatabase<any>, ticketId: string, authorId: string, authorName: string, ticketStatus: string, input: RepairNoteCreateInput) {
|
||||||
|
const [note] = await db
|
||||||
|
.insert(repairNotes)
|
||||||
|
.values({
|
||||||
|
repairTicketId: ticketId,
|
||||||
|
authorId,
|
||||||
|
authorName,
|
||||||
|
content: input.content,
|
||||||
|
visibility: input.visibility,
|
||||||
|
ticketStatus: ticketStatus as any,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
return note
|
||||||
|
},
|
||||||
|
|
||||||
|
async listByTicket(db: PostgresJsDatabase<any>, ticketId: string) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(repairNotes)
|
||||||
|
.where(eq(repairNotes.repairTicketId, ticketId))
|
||||||
|
.orderBy(repairNotes.createdAt)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||||
|
const [note] = await db
|
||||||
|
.delete(repairNotes)
|
||||||
|
.where(eq(repairNotes.id, id))
|
||||||
|
.returning()
|
||||||
|
return note ?? null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export {
|
|||||||
RepairBatchCreateSchema,
|
RepairBatchCreateSchema,
|
||||||
RepairBatchUpdateSchema,
|
RepairBatchUpdateSchema,
|
||||||
RepairBatchStatusUpdateSchema,
|
RepairBatchStatusUpdateSchema,
|
||||||
|
RepairNoteVisibility,
|
||||||
|
RepairNoteCreateSchema,
|
||||||
RepairServiceTemplateCreateSchema,
|
RepairServiceTemplateCreateSchema,
|
||||||
RepairServiceTemplateUpdateSchema,
|
RepairServiceTemplateUpdateSchema,
|
||||||
} from './repairs.schema.js'
|
} from './repairs.schema.js'
|
||||||
@@ -93,6 +95,7 @@ export type {
|
|||||||
RepairBatchCreateInput,
|
RepairBatchCreateInput,
|
||||||
RepairBatchUpdateInput,
|
RepairBatchUpdateInput,
|
||||||
RepairBatchStatusUpdateInput,
|
RepairBatchStatusUpdateInput,
|
||||||
|
RepairNoteCreateInput,
|
||||||
RepairServiceTemplateCreateInput,
|
RepairServiceTemplateCreateInput,
|
||||||
RepairServiceTemplateUpdateInput,
|
RepairServiceTemplateUpdateInput,
|
||||||
} from './repairs.schema.js'
|
} from './repairs.schema.js'
|
||||||
|
|||||||
@@ -97,6 +97,17 @@ export const RepairBatchStatusUpdateSchema = z.object({
|
|||||||
})
|
})
|
||||||
export type RepairBatchStatusUpdateInput = z.infer<typeof RepairBatchStatusUpdateSchema>
|
export type RepairBatchStatusUpdateInput = z.infer<typeof RepairBatchStatusUpdateSchema>
|
||||||
|
|
||||||
|
// --- Repair Note schemas ---
|
||||||
|
|
||||||
|
export const RepairNoteVisibility = z.enum(['internal', 'customer'])
|
||||||
|
export type RepairNoteVisibility = z.infer<typeof RepairNoteVisibility>
|
||||||
|
|
||||||
|
export const RepairNoteCreateSchema = z.object({
|
||||||
|
content: z.string().min(1),
|
||||||
|
visibility: RepairNoteVisibility.default('internal'),
|
||||||
|
})
|
||||||
|
export type RepairNoteCreateInput = z.infer<typeof RepairNoteCreateSchema>
|
||||||
|
|
||||||
// --- Repair Service Template schemas ---
|
// --- Repair Service Template schemas ---
|
||||||
|
|
||||||
export const RepairServiceTemplateCreateSchema = z.object({
|
export const RepairServiceTemplateCreateSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user