- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints. - Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete. - Created VendorContext for managing vendor state across components. - Developed VendorGeneralInfo component to display detailed vendor information. - Introduced AedSymbol and Money components for consistent currency representation. - Added PromptDialog for user input prompts throughout the application. - Implemented RelationLink component for unified related-data display in CRUD tables. - Created InspectionTemplatesClient for API interactions related to inspection templates.
208 lines
7.7 KiB
TypeScript
208 lines
7.7 KiB
TypeScript
"use client"
|
|
|
|
import React, { useRef, useState } from "react"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/components/ui/dialog"
|
|
import { Download, FileSpreadsheet, Loader2, UploadCloud } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { ImportResultsDialog } from "@/shared/components/import-results-dialog"
|
|
import type { ImportDataResponse } from "@garage/api"
|
|
|
|
type ImportDataButtonProps = {
|
|
onImport: (file: File) => Promise<ImportDataResponse>
|
|
onSuccess?: () => void
|
|
accept?: string
|
|
label?: string
|
|
entityLabel?: string
|
|
onDownloadSample?: () => Promise<Blob>
|
|
sampleFileName?: string
|
|
notes?: React.ReactNode[]
|
|
}
|
|
|
|
const DEFAULT_NOTES: React.ReactNode[] = [
|
|
"Download the sample file and use it as a template — keep the column headers exactly as provided.",
|
|
"Supported file formats: .xlsx, .xls and .csv.",
|
|
"Fields marked as required in the sample must not be empty.",
|
|
"Existing records will not be overwritten. Each row is imported as a new record.",
|
|
"After uploading, you'll see a results summary with any rows that failed and why.",
|
|
]
|
|
|
|
export function ImportDataButton({
|
|
onImport,
|
|
onSuccess,
|
|
accept = ".xlsx,.xls,.csv",
|
|
label = "Import",
|
|
entityLabel,
|
|
onDownloadSample,
|
|
sampleFileName,
|
|
notes,
|
|
}: ImportDataButtonProps) {
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const [stepsOpen, setStepsOpen] = useState(false)
|
|
const [isImporting, setIsImporting] = useState(false)
|
|
const [isDownloadingSample, setIsDownloadingSample] = useState(false)
|
|
const [result, setResult] = useState<ImportDataResponse["data"] | null>(null)
|
|
const [resultOpen, setResultOpen] = useState(false)
|
|
|
|
const effectiveNotes = notes && notes.length > 0 ? notes : DEFAULT_NOTES
|
|
const entityForCopy = entityLabel ?? "records"
|
|
|
|
const handleDownloadSample = async () => {
|
|
if (!onDownloadSample) return
|
|
setIsDownloadingSample(true)
|
|
try {
|
|
const blob = await onDownloadSample()
|
|
const url = URL.createObjectURL(blob)
|
|
const anchor = document.createElement("a")
|
|
anchor.href = url
|
|
anchor.download = `${sampleFileName ?? "import-sample"}.xlsx`
|
|
document.body.appendChild(anchor)
|
|
anchor.click()
|
|
document.body.removeChild(anchor)
|
|
URL.revokeObjectURL(url)
|
|
toast.success("Sample downloaded successfully")
|
|
} catch (err: any) {
|
|
toast.error(err?.message ?? "Failed to download sample")
|
|
} finally {
|
|
setIsDownloadingSample(false)
|
|
}
|
|
}
|
|
|
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
|
|
setIsImporting(true)
|
|
try {
|
|
const response = await onImport(file)
|
|
const data = response.data ?? { imported_count: 0, failed_count: 0, failed_rows: [] }
|
|
setResult(data)
|
|
setStepsOpen(false)
|
|
setResultOpen(true)
|
|
|
|
if (data.failed_count === 0) {
|
|
onSuccess?.()
|
|
} else if (data.imported_count > 0) {
|
|
onSuccess?.()
|
|
}
|
|
} catch (err: any) {
|
|
toast.error(err?.message ?? "Failed to import data")
|
|
} finally {
|
|
setIsImporting(false)
|
|
if (inputRef.current) inputRef.current.value = ""
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept={accept}
|
|
className="hidden"
|
|
onChange={handleFileChange}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setStepsOpen(true)}
|
|
>
|
|
<Download />
|
|
{label}
|
|
</Button>
|
|
|
|
<Dialog
|
|
open={stepsOpen}
|
|
onOpenChange={(open) => {
|
|
if (isImporting) return
|
|
setStepsOpen(open)
|
|
}}
|
|
>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>Import {entityForCopy.toLowerCase()}</DialogTitle>
|
|
<DialogDescription>
|
|
Follow these steps to import {entityForCopy.toLowerCase()} from a spreadsheet.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{onDownloadSample && (
|
|
<div className="flex items-start gap-3 rounded-md border bg-muted/30 p-3">
|
|
<FileSpreadsheet className="mt-0.5 size-5 shrink-0 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Step 1 — Download the sample file</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Use it as a template so columns are recognized correctly.
|
|
</p>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isDownloadingSample}
|
|
onClick={handleDownloadSample}
|
|
>
|
|
{isDownloadingSample ? (
|
|
<Loader2 className="animate-spin" />
|
|
) : (
|
|
<Download />
|
|
)}
|
|
Sample
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<p className="mb-2 text-sm font-medium">
|
|
{onDownloadSample ? "Step 2 — Notes before you upload" : "Notes before you upload"}
|
|
</p>
|
|
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
|
|
{effectiveNotes.map((note, idx) => (
|
|
<li key={idx}>{note}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setStepsOpen(false)}
|
|
disabled={isImporting}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
disabled={isImporting}
|
|
onClick={() => inputRef.current?.click()}
|
|
>
|
|
{isImporting ? (
|
|
<Loader2 className="animate-spin" />
|
|
) : (
|
|
<UploadCloud />
|
|
)}
|
|
{isImporting ? "Uploading..." : "Choose file & upload"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{result && (
|
|
<ImportResultsDialog
|
|
open={resultOpen}
|
|
onOpenChange={setResultOpen}
|
|
importedCount={result.imported_count}
|
|
failedCount={result.failed_count}
|
|
failedRows={result.failed_rows}
|
|
entityLabel={entityLabel}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|