garage-erp/apps/dashboard/shared/components/import-data-button.tsx
humam kerdiah 4bfd8c84a9 feat: add template checkpoint edit dialog and vendor management components
- 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.
2026-05-18 12:08:42 +04:00

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}
/>
)}
</>
)
}