diff --git a/packages/admin/src/components/repairs/ticket-notes.tsx b/packages/admin/src/components/repairs/ticket-notes.tsx index db80e19..c3314c0 100644 --- a/packages/admin/src/components/repairs/ticket-notes.tsx +++ b/packages/admin/src/components/repairs/ticket-notes.tsx @@ -1,11 +1,13 @@ -import { useState } from 'react' +import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { queryOptions } from '@tanstack/react-query' import { repairNoteListOptions, repairNoteMutations, repairNoteKeys } from '@/api/repairs' +import { api } from '@/lib/api-client' 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 { Send, Trash2, Eye, Lock, ImageIcon, X } from 'lucide-react' import { toast } from 'sonner' import type { RepairNote } from '@/types/repair' @@ -23,6 +25,29 @@ const STATUS_LABELS: Record = { cancelled: 'Cancelled', } +interface FileRecord { + id: string + path: string + filename: string +} + +function noteFilesOptions(noteId: string) { + return queryOptions({ + queryKey: ['files', 'repair_note', noteId], + queryFn: () => api.get<{ data: FileRecord[] }>('/v1/files', { entityType: 'repair_note', entityId: noteId }), + enabled: !!noteId, + }) +} + +async function openSignedFile(fileId: string) { + try { + const res = await api.get<{ url: string }>(`/v1/files/signed-url/${fileId}`) + window.open(res.url, '_blank') + } catch { + toast.error('Failed to open file') + } +} + interface TicketNotesProps { ticketId: string } @@ -30,22 +55,16 @@ interface TicketNotesProps { export function TicketNotes({ ticketId }: TicketNotesProps) { const queryClient = useQueryClient() const hasPermission = useAuthStore((s) => s.hasPermission) + const token = useAuthStore((s) => s.token) const [content, setContent] = useState('') const [visibility, setVisibility] = useState<'internal' | 'customer'>('internal') + const [photos, setPhotos] = useState([]) + const [posting, setPosting] = useState(false) + const photoInputRef = useRef(null) 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: () => { @@ -55,10 +74,48 @@ export function TicketNotes({ ticketId }: TicketNotesProps) { onError: (err) => toast.error(err.message), }) - function handleSubmit(e: React.FormEvent) { + async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!content.trim()) return - createMutation.mutate({ content: content.trim(), visibility }) + setPosting(true) + + try { + // Create the note + const note = await repairNoteMutations.create(ticketId, { content: content.trim(), visibility }) + + // Upload attached photos to the note + for (const photo of photos) { + const formData = new FormData() + formData.append('file', photo) + formData.append('entityType', 'repair_note') + formData.append('entityId', note.id) + formData.append('category', 'attachment') + await fetch('/v1/files', { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }) + } + + queryClient.invalidateQueries({ queryKey: repairNoteKeys.all(ticketId) }) + setContent('') + setPhotos([]) + toast.success('Note added') + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to post note') + } finally { + setPosting(false) + } + } + + function addPhotos(e: React.ChangeEvent) { + const files = Array.from(e.target.files ?? []) + setPhotos((prev) => [...prev, ...files]) + e.target.value = '' + } + + function removePhoto(index: number) { + setPhotos((prev) => prev.filter((_, i) => i !== index)) } function formatDate(dateStr: string) { @@ -78,6 +135,25 @@ export function TicketNotes({ ticketId }: TicketNotesProps) { placeholder="Add a note..." rows={3} /> + + {/* Photo previews */} + {photos.length > 0 && ( +
+ {photos.map((photo, i) => ( +
+ + +
+ ))} +
+ )} +
+
-
+ + )} @@ -133,10 +225,13 @@ function NoteEntry({ note, formatDate, canDelete, onDelete }: { canDelete: boolean onDelete: () => void }) { + const { data: filesData } = useQuery(noteFilesOptions(note.id)) + const photos = filesData?.data ?? [] + return (
-
+
{note.authorName} {formatDate(note.createdAt)} {note.ticketStatus && ( @@ -162,6 +257,21 @@ function NoteEntry({ note, formatDate, canDelete, onDelete }: { )}

{note.content}

+ + {/* Inline photos */} + {photos.length > 0 && ( +
+ {photos.map((photo) => ( + + ))} +
+ )}
) }