"use client"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet"; import { toast } from "sonner"; import { Loader2, User, Building2, Mail, Phone, AtSign, MapPin, FileText } from "lucide-react"; import getPocketBase from "@/lib/pocketbase"; import { getAllCompanies } from "@/lib/pb-helpers"; import { CompanySearch } from "@/components/ui/company-search"; import { UserSearch } from "@/components/ui/user-search"; import type { Company, Customer } from "@/lib/types"; interface CustomerFormProps { open: boolean; onOpenChange: (open: boolean) => void; customer?: Customer | null; onSaved: () => void; } /** * Treat PB's `{ lon: 0, lat: 0 }` (Null Island) as "no coordinates". * PB geoPoint is non-nullable with this zero default — masking it * keeps every unset customer from showing up at sea. */ function hasRealCoordinates(c: Customer["coordinates"]): c is { lon: number; lat: number } { return !!c && (c.lon !== 0 || c.lat !== 0); } export function CustomerForm({ open, onOpenChange, customer, onSaved }: CustomerFormProps) { const [loading, setLoading] = useState(false); const [companies, setCompanies] = useState([]); const [form, setForm] = useState({ customer: "", email: "", alternative_email: "", buyer_email: "", number: "", company: "", rep: "", lat: "", lon: "", description: "", }); // Lazy-load companies for the company combobox the first time the // sheet opens. Cheap (small table) and the user opens the form // infrequently, so we don't bother with a context or page-level // prefetch. useEffect(() => { if (!open || companies.length > 0) return; let cancelled = false; getAllCompanies() .then((data) => { if (!cancelled) setCompanies(data); }) .catch(() => { // best-effort — combobox will just show empty list }); return () => { cancelled = true; }; }, [open, companies.length]); useEffect(() => { if (customer) { setForm({ customer: customer.customer || "", email: customer.email || "", alternative_email: customer.alternative_email || "", buyer_email: customer.buyer_email || "", number: customer.number || "", company: customer.company || "", rep: customer.rep || "", lat: hasRealCoordinates(customer.coordinates) ? String(customer.coordinates.lat) : "", lon: hasRealCoordinates(customer.coordinates) ? String(customer.coordinates.lon) : "", description: customer.description || "", }); } else { setForm({ customer: "", email: "", alternative_email: "", buyer_email: "", number: "", company: "", rep: "", lat: "", lon: "", description: "", }); } }, [customer, open]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!form.customer.trim()) { toast.error("Customer name is required"); return; } // Coordinates: blank → PB default {0,0}; both-or-neither, range-checked. const latTrim = form.lat.trim(); const lonTrim = form.lon.trim(); let coordinatesPayload: { lon: number; lat: number }; if (!latTrim && !lonTrim) { coordinatesPayload = { lon: 0, lat: 0 }; } else { if (!latTrim || !lonTrim) { toast.error("Enter both latitude and longitude, or leave both blank."); return; } const lat = Number(latTrim); const lon = Number(lonTrim); if (!Number.isFinite(lat) || !Number.isFinite(lon)) { toast.error("Coordinates must be numbers."); return; } if (lat < -90 || lat > 90) { toast.error("Latitude must be between -90 and 90."); return; } if (lon < -180 || lon > 180) { toast.error("Longitude must be between -180 and 180."); return; } coordinatesPayload = { lon, lat }; } setLoading(true); try { const pb = getPocketBase(); // `company` and `rep` are relation ids — PB stores them as // strings; passing "" unsets the relation. `dateadded` / // `lastvisiteddate` mirror the legacy fields (still useful for // sorting / display on the list page). const data = { customer: form.customer, email: form.email, alternative_email: form.alternative_email, buyer_email: form.buyer_email, number: form.number, company: form.company, rep: form.rep, coordinates: coordinatesPayload, description: form.description, dateadded: customer?.dateadded || new Date().toISOString(), lastvisiteddate: new Date().toISOString(), }; if (customer) { await pb.collection("customers").update(customer.id, data); toast.success("Customer updated"); } else { await pb.collection("customers").create(data); toast.success("Customer created"); } onSaved(); onOpenChange(false); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Failed to save customer"; toast.error(message); } finally { setLoading(false); } }; const FieldLabel = ({ icon: Icon, label }: { icon: React.ElementType; label: string }) => ( ); return (
{customer ? "Edit Customer" : "New Customer"}
{customer ? "Update customer details" : "Add a new customer to your CRM"}
setForm({ ...form, customer: e.target.value })} required placeholder="Full name or business name" className="h-9" />
setForm({ ...form, company: id })} companies={companies} placeholder="Select company..." />

The company the customer works for. Delivery address is captured below — different shafts may share the same company but deliver to different coordinates.

Contact

setForm({ ...form, email: e.target.value })} placeholder="email@example.com" className="h-9" />
setForm({ ...form, number: e.target.value })} placeholder="Phone number" className="h-9" />
setForm({ ...form, alternative_email: e.target.value })} placeholder="alternative@example.com" className="h-9" />
setForm({ ...form, buyer_email: e.target.value })} placeholder="buyer@example.com" className="h-9" />

Details

setForm({ ...form, rep: id })} placeholder="Select sales rep..." />
setForm({ ...form, lat: e.target.value })} type="number" step="any" placeholder="Latitude" className="h-9" aria-label="Latitude" /> setForm({ ...form, lon: e.target.value })} type="number" step="any" placeholder="Longitude" className="h-9" aria-label="Longitude" />

Leave both blank if unknown. Used later for routing / delivery. Example: -26.2041, 28.0473 (Johannesburg).