diff --git a/src/app/(rep)/rep/check-in-view.tsx b/src/app/(rep)/rep/check-in-view.tsx index aabcf6b..fa90063 100644 --- a/src/app/(rep)/rep/check-in-view.tsx +++ b/src/app/(rep)/rep/check-in-view.tsx @@ -1,11 +1,27 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { MapPin, Search, Check, WifiOff, RefreshCw, Loader2, Navigation } from "lucide-react"; +import { + MapPin, + Search, + Check, + WifiOff, + RefreshCw, + Loader2, + Navigation, + UserPlus, +} from "lucide-react"; import { toast } from "sonner"; import getPocketBase from "@/lib/pocketbase"; import { rankNearbyCustomers, DEFAULT_RADIUS_KM } from "@/lib/visit-matching"; import type { CustomerSnapshotRow } from "@/lib/server/visits-service"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; import { enqueue, flushOutbox, @@ -14,6 +30,12 @@ import { writeCachedSnapshot, } from "./outbox"; import { formatSnapshotAge, isSnapshotStale } from "./snapshot-freshness"; +import { + sortCustomersForPicker, + filterPickerCustomers, + type PickerCustomer, +} from "./picker-sort"; +import { formatVisitToast } from "./visit-toast"; /** Radii the "widen search" button steps through, in km. */ const RADIUS_STEPS = [DEFAULT_RADIUS_KM, 5, 15]; @@ -35,6 +57,8 @@ export function CheckInView() { const [selected, setSelected] = useState>(new Set()); const [pinning, setPinning] = useState>(new Set()); const [search, setSearch] = useState(""); + const [pickerOpen, setPickerOpen] = useState(false); + const [pickerSearch, setPickerSearch] = useState(""); const [notes, setNotes] = useState(""); const [submitting, setSubmitting] = useState(false); const [pending, setPending] = useState(0); @@ -233,6 +257,32 @@ export function CheckInView() { .slice(0, 20); }, [search, customers, nearby]); + /* ------------------------------------------------------------- picker */ + + // Sorted once (unpinned first — that's the primary picker use-case, + // pinning a customer who isn't on the nearby list), then filtered by + // the sheet's own search term. Both steps live in pure helpers so the + // rule is testable without the sheet/React on top. + const pickerRows = useMemo( + () => sortCustomersForPicker(customers), + [customers] + ); + + const pickerResults = useMemo( + () => filterPickerCustomers(pickerRows, pickerSearch), + [pickerRows, pickerSearch] + ); + + const openPicker = useCallback(() => { + setPickerSearch(""); + setPickerOpen(true); + }, []); + + const closePicker = useCallback(() => { + setPickerOpen(false); + setPickerSearch(""); + }, []); + const toggle = useCallback( (id: string, hasCoordinates: boolean) => { setSelected((prev) => { @@ -275,6 +325,7 @@ export function CheckInView() { }); const count = selected.size; + const pinnedCount = pinning.size; setSelected(new Set()); setPinning(new Set()); setNotes(""); @@ -285,13 +336,24 @@ export function CheckInView() { try { const result = await flushOutbox(pb.authStore.token); refreshPending(); - if (result.sent > 0) { - toast.success(`${count} visit${count === 1 ? "" : "s"} logged`); - } else { - toast.success(`${count} visit${count === 1 ? "" : "s"} saved — will sync when you have signal`); - } + toast.success( + formatVisitToast({ + count, + pinned: pinnedCount, + delivered: result.sent > 0, + }) + ); } catch { - toast.success(`${count} visit${count === 1 ? "" : "s"} saved offline`); + // Network or unexpected failure — entry is still queued and will + // be retried by the next flush; surface the offline variant so + // the rep knows coordinates will land when signal comes back. + toast.success( + formatVisitToast({ + count, + pinned: pinnedCount, + delivered: false, + }) + ); } finally { void entry; setSubmitting(false); @@ -402,8 +464,9 @@ export function CheckInView() { {nearby.length === 0 ? (

- No customers pinned within {radiusKm} km yet. Search below — saving a - visit records this spot for next time. + No customers pinned within {radiusKm} km yet. Use{" "} + Add a customer{" "} + below — saving a visit records this spot for next time.

) : (