|
|
@@ -39,7 +39,6 @@ import {
|
|
|
} from '@/components/ui/command'
|
|
|
import { Switch } from '@/components/ui/switch'
|
|
|
import {
|
|
|
- ArrowLeft,
|
|
|
Camera,
|
|
|
Check,
|
|
|
ChevronsUpDown,
|
|
|
@@ -460,6 +459,9 @@ export function ServiceRecordForm({
|
|
|
} else {
|
|
|
router.refresh()
|
|
|
}
|
|
|
+ } else if (result.data?.id) {
|
|
|
+ router.push(`/vehicles/${vehicleId}/service/${result.data.id}/edit`)
|
|
|
+ router.refresh()
|
|
|
} else {
|
|
|
router.push(`/vehicles/${vehicleId}`)
|
|
|
router.refresh()
|
|
|
@@ -477,22 +479,6 @@ export function ServiceRecordForm({
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-6">
|
|
|
- {!isEdit && (
|
|
|
- <div>
|
|
|
- <Link
|
|
|
- href={`/vehicles/${vehicleId}`}
|
|
|
- className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
|
- >
|
|
|
- <ArrowLeft className="h-4 w-4" />
|
|
|
- Back to {vehicleName}
|
|
|
- </Link>
|
|
|
- <div>
|
|
|
- <h1 className="text-2xl font-bold">New Service Record</h1>
|
|
|
- <p className="mt-1 text-sm text-muted-foreground">{vehicleName}</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
<form id="service-record-form" onSubmit={handleSubmit} className="space-y-6">
|
|
|
{/* Top row: Basic Info | (Diagnostic Notes if new) | Totals */}
|
|
|
<div className={`grid grid-cols-1 gap-6 ${isEdit ? 'lg:grid-cols-2' : 'lg:grid-cols-3'}`}>
|
|
|
@@ -750,281 +736,6 @@ export function ServiceRecordForm({
|
|
|
</Card>
|
|
|
</div>
|
|
|
|
|
|
- {/* Service Images — only shown for new records (managed on Images tab for edits) */}
|
|
|
- {!isEdit && (
|
|
|
- <Card className="border-0 shadow-sm">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle className="flex items-center gap-2 text-base">
|
|
|
- <Camera className="h-4 w-4" />
|
|
|
- Service Images
|
|
|
- </CardTitle>
|
|
|
- </CardHeader>
|
|
|
- <CardContent className="space-y-4">
|
|
|
- <div
|
|
|
- onDrop={handleDropImages}
|
|
|
- onDragOver={(e) => e.preventDefault()}
|
|
|
- onClick={() => imageInputRef.current?.click()}
|
|
|
- className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
|
|
|
- >
|
|
|
- <ImageIcon className="mb-2 h-8 w-8 text-muted-foreground/50" />
|
|
|
- <p className="text-sm font-medium">
|
|
|
- {uploadingImages ? 'Uploading...' : 'Drop images here or click to browse'}
|
|
|
- </p>
|
|
|
- <p className="mt-1 text-xs text-muted-foreground">JPG, PNG, WebP — max 10MB each</p>
|
|
|
- <input
|
|
|
- ref={imageInputRef}
|
|
|
- type="file"
|
|
|
- multiple
|
|
|
- accept=".jpg,.jpeg,.png,.webp"
|
|
|
- className="hidden"
|
|
|
- onChange={(e) => {
|
|
|
- if (e.target.files && e.target.files.length > 0) {
|
|
|
- handleFileUpload(e.target.files, 'image')
|
|
|
- e.target.value = ''
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {uploadingImages && (
|
|
|
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
- <Loader2 className="h-4 w-4 animate-spin" />
|
|
|
- Uploading images...
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {serviceImages.length > 0 && (
|
|
|
- <div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-6">
|
|
|
- {serviceImages.map((file, i) => (
|
|
|
- <div key={i} className="group overflow-hidden rounded-lg border">
|
|
|
- <div className="relative">
|
|
|
- <img
|
|
|
- src={file.fileUrl}
|
|
|
- alt={file.description || file.fileName}
|
|
|
- className="aspect-square w-full object-cover"
|
|
|
- />
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="secondary"
|
|
|
- size="icon"
|
|
|
- className="absolute right-1 top-1 h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100"
|
|
|
- onClick={() => removeServiceImage(i)}
|
|
|
- >
|
|
|
- <X className="h-3 w-3" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- <div className="space-y-1 p-1.5">
|
|
|
- <Input
|
|
|
- placeholder="Description..."
|
|
|
- value={file.description || ''}
|
|
|
- onChange={(e) => {
|
|
|
- setServiceImages((prev) => {
|
|
|
- const updated = [...prev]
|
|
|
- updated[i] = { ...updated[i], description: e.target.value }
|
|
|
- return updated
|
|
|
- })
|
|
|
- }}
|
|
|
- className="h-7 text-xs"
|
|
|
- />
|
|
|
- <label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
- <Switch
|
|
|
- checked={file.includeInInvoice !== false}
|
|
|
- onCheckedChange={(checked) => {
|
|
|
- setServiceImages((prev) => {
|
|
|
- const updated = [...prev]
|
|
|
- updated[i] = { ...updated[i], includeInInvoice: checked }
|
|
|
- return updated
|
|
|
- })
|
|
|
- }}
|
|
|
- className="scale-75"
|
|
|
- />
|
|
|
- Invoice
|
|
|
- </label>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* Diagnostic Reports + Documents — only shown for new records */}
|
|
|
- {/* In edit mode, both are managed on the Documents tab */}
|
|
|
- {!isEdit && (
|
|
|
- <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
|
- {/* Diagnostic Reports */}
|
|
|
- <Card className="border-0 shadow-sm">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle className="flex items-center gap-2 text-base">
|
|
|
- <FileText className="h-4 w-4" />
|
|
|
- Diagnostic Reports
|
|
|
- </CardTitle>
|
|
|
- </CardHeader>
|
|
|
- <CardContent className="space-y-4">
|
|
|
- <div
|
|
|
- onDrop={handleDropReports}
|
|
|
- onDragOver={(e) => e.preventDefault()}
|
|
|
- onClick={() => reportInputRef.current?.click()}
|
|
|
- className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
|
|
|
- >
|
|
|
- <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
|
|
|
- <p className="text-sm font-medium">
|
|
|
- {uploadingReports ? 'Uploading...' : 'Drop files here or click to browse'}
|
|
|
- </p>
|
|
|
- <p className="mt-1 text-xs text-muted-foreground">
|
|
|
- PDF, CSV, TXT — max 10MB each
|
|
|
- </p>
|
|
|
- <input
|
|
|
- ref={reportInputRef}
|
|
|
- type="file"
|
|
|
- multiple
|
|
|
- accept=".pdf,.csv,.txt"
|
|
|
- className="hidden"
|
|
|
- onChange={(e) => {
|
|
|
- if (e.target.files && e.target.files.length > 0) {
|
|
|
- handleFileUpload(e.target.files, 'diagnostic')
|
|
|
- e.target.value = ''
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {uploadingReports && (
|
|
|
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
- <Loader2 className="h-4 w-4 animate-spin" />
|
|
|
- Uploading reports...
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {diagnosticReports.length > 0 && (
|
|
|
- <div className="space-y-2">
|
|
|
- {diagnosticReports.map((file, i) => (
|
|
|
- <div key={i} className="flex items-center gap-3 rounded-md border p-2.5">
|
|
|
- {getFileIcon(file.fileType)}
|
|
|
- <div className="min-w-0 flex-1">
|
|
|
- <p className="truncate text-sm font-medium">{file.fileName}</p>
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
- {formatFileSize(file.fileSize)}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
- <Switch
|
|
|
- checked={file.includeInInvoice !== false}
|
|
|
- onCheckedChange={(checked) => {
|
|
|
- setDiagnosticReports((prev) => {
|
|
|
- const updated = [...prev]
|
|
|
- updated[i] = { ...updated[i], includeInInvoice: checked }
|
|
|
- return updated
|
|
|
- })
|
|
|
- }}
|
|
|
- className="scale-75"
|
|
|
- />
|
|
|
- Invoice
|
|
|
- </label>
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="ghost"
|
|
|
- size="icon"
|
|
|
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
|
|
- onClick={() => removeDiagnosticReport(i)}
|
|
|
- >
|
|
|
- <X className="h-3.5 w-3.5" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
-
|
|
|
- {/* Documents */}
|
|
|
- <Card className="border-0 shadow-sm">
|
|
|
- <CardHeader>
|
|
|
- <CardTitle className="flex items-center gap-2 text-base">
|
|
|
- <Paperclip className="h-4 w-4" />
|
|
|
- Documents
|
|
|
- </CardTitle>
|
|
|
- </CardHeader>
|
|
|
- <CardContent className="space-y-4">
|
|
|
- <div
|
|
|
- onDrop={handleDropDocuments}
|
|
|
- onDragOver={(e) => e.preventDefault()}
|
|
|
- onClick={() => documentInputRef.current?.click()}
|
|
|
- className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-6 transition-colors hover:border-muted-foreground/50"
|
|
|
- >
|
|
|
- <Upload className="mb-2 h-8 w-8 text-muted-foreground/50" />
|
|
|
- <p className="text-sm font-medium">
|
|
|
- {uploadingDocuments ? 'Uploading...' : 'Drop files here or click to browse'}
|
|
|
- </p>
|
|
|
- <p className="mt-1 text-xs text-muted-foreground">
|
|
|
- PDF, DOCX, XLSX, CSV, TXT — max 10MB each
|
|
|
- </p>
|
|
|
- <input
|
|
|
- ref={documentInputRef}
|
|
|
- type="file"
|
|
|
- multiple
|
|
|
- accept=".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
|
|
|
- className="hidden"
|
|
|
- onChange={(e) => {
|
|
|
- if (e.target.files && e.target.files.length > 0) {
|
|
|
- handleFileUpload(e.target.files, 'document')
|
|
|
- e.target.value = ''
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {uploadingDocuments && (
|
|
|
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
- <Loader2 className="h-4 w-4 animate-spin" />
|
|
|
- Uploading documents...
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {documents.length > 0 && (
|
|
|
- <div className="space-y-2">
|
|
|
- {documents.map((file, i) => (
|
|
|
- <div key={i} className="flex items-center gap-3 rounded-md border p-2.5">
|
|
|
- {getFileIcon(file.fileType)}
|
|
|
- <div className="min-w-0 flex-1">
|
|
|
- <p className="truncate text-sm font-medium">{file.fileName}</p>
|
|
|
- <p className="text-xs text-muted-foreground">
|
|
|
- {formatFileSize(file.fileSize)}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <label className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
- <Switch
|
|
|
- checked={file.includeInInvoice !== false}
|
|
|
- onCheckedChange={(checked) => {
|
|
|
- setDocuments((prev) => {
|
|
|
- const updated = [...prev]
|
|
|
- updated[i] = { ...updated[i], includeInInvoice: checked }
|
|
|
- return updated
|
|
|
- })
|
|
|
- }}
|
|
|
- className="scale-75"
|
|
|
- />
|
|
|
- Invoice
|
|
|
- </label>
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="ghost"
|
|
|
- size="icon"
|
|
|
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
|
|
- onClick={() => removeDocument(i)}
|
|
|
- >
|
|
|
- <X className="h-3.5 w-3.5" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
{/* Parts Table — full width */}
|
|
|
<Card className="border-0 shadow-sm">
|
|
|
<CardHeader className="flex flex-row items-center justify-between">
|