"use client" import { useState, useRef } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Upload, FileText, CheckCircle, XCircle, Download } from "lucide-react" interface Tenant { id: string; first_name: string; last_name: string; unit?: any; property?: any } interface Property { id: string; name: string } interface ParsedRow { tenant_id: string property_id: string amount: number due_date: string status: string payment_method?: string notes?: string _tenantName?: string _error?: string } const SAMPLE_CSV = `tenant_id,property_id,amount,due_date,status,payment_method,notes TENANT_UUID_HERE,PROPERTY_UUID_HERE,1500.00,2026-05-01,pending,, TENANT_UUID_HERE,PROPERTY_UUID_HERE,1500.00,2026-04-01,paid,bank_transfer,April rent` export function RentCsvImport({ tenants, properties }: { tenants: Tenant[]; properties: Property[] }) { const router = useRouter() const fileRef = useRef(null) const [rows, setRows] = useState([]) const [importing, setImporting] = useState(false) const [done, setDone] = useState(false) function downloadSample() { const blob = new Blob([SAMPLE_CSV], { type: "text/csv" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url; a.download = "rent-import-sample.csv"; a.click() URL.revokeObjectURL(url) } async function handleFile(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return const { parse } = await import("papaparse") parse(file, { header: true, skipEmptyLines: true, complete: (result) => { const parsed: ParsedRow[] = (result.data as any[]).map((row) => { const tenant = tenants.find((t) => t.id === row.tenant_id?.trim()) const amount = parseFloat(row.amount) const errors: string[] = [] if (!tenant) errors.push("tenant not found") if (isNaN(amount) || amount <= 0) errors.push("invalid amount") if (!row.due_date?.trim()) errors.push("missing due_date") if (!row.property_id?.trim()) errors.push("missing property_id") return { tenant_id: row.tenant_id?.trim(), property_id: row.property_id?.trim(), amount, due_date: row.due_date?.trim(), status: row.status?.trim() || "pending", payment_method: row.payment_method?.trim() || undefined, notes: row.notes?.trim() || undefined, _tenantName: tenant ? `${tenant.first_name} ${tenant.last_name}` : row.tenant_id, _error: errors.length ? errors.join(", ") : undefined, } }) setRows(parsed) setDone(false) }, }) } async function handleImport() { const valid = rows.filter((r) => !r._error) if (!valid.length) { toast.error("No valid rows to import"); return } setImporting(true) let success = 0, failed = 0 for (const row of valid) { const res = await fetch("/api/rent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tenant_id: row.tenant_id, property_id: row.property_id, amount: row.amount, due_date: row.due_date, status: row.status, payment_method: row.payment_method, notes: row.notes, }), }) res.ok ? success++ : failed++ } setImporting(false) setDone(true) toast.success(`Imported ${success} payment${success !== 1 ? "s" : ""}${failed ? `, ${failed} failed` : ""}`) if (success > 0) router.refresh() } const validCount = rows.filter((r) => !r._error).length const invalidCount = rows.filter((r) => !!r._error).length return (
{/* Instructions */}

How it works

  1. Download the sample CSV template
  2. Fill in tenant_id and property_id from your dashboard
  3. Upload the filled CSV file
  4. Review rows and click Import
{/* Upload */}
fileRef.current?.click()} className="flex cursor-pointer flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed border-white/10 bg-white/[0.02] p-10 hover:border-indigo-500/40 hover:bg-indigo-500/5 transition" >

Click to upload CSV file

{/* Preview */} {rows.length > 0 && (

{rows.length} rows parsed

{validCount > 0 && {validCount} valid} {invalidCount > 0 && {invalidCount} invalid}
{rows.map((row, i) => (
{row._error ? : }

{row._tenantName}

{row.due_date} · ${row.amount}

{row._error &&

{row._error}

}
{row.status}
))}
{!done && (
)} {done && (
Import complete!
)}
)}
) }