import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Mail, Package, Pencil, Phone, Plus, Users } from 'lucide-react'; import { Avatar, Button, DataTable, EmptyState, Field, Grid, IconButton, Input, KpiCard, Modal, PageHeader, Stack, } from '@/components/ds'; import { ListToolbar } from '@/components/doormile/ListToolbar'; import { useTenantCustomers, useUpdateTenantCustomer } from '@/lib/doormileHooks'; import { matchesQuery, number, percentOf, useDebouncedValue } from '@/lib/doormileFormat'; /** * Customers — the end customers a client's bookings are delivered to. * * The identifier and name fields differ between the two customer resources * this list can be fed from, so both are read through the accessors below * rather than assumed: a row that arrives as `firstname`/`lastname` and one * that arrives as `name` have to render identically. */ const custId = (row) => row.appcustomerid ?? row.tenantcustomerid ?? row.customerid ?? row.id; const custName = (row) => row.name || [row.firstname, row.lastname].filter(Boolean).join(' ') || '—'; const custPhone = (row) => row.phone || row.contactno || ''; const custEmail = (row) => row.email || ''; /** Tenant customers carry no booking count; 0 is honest rather than blank. */ const custBookings = (row) => row.totalbookings ?? 0; export default function Customers({ embedded = false }) { const navigate = useNavigate(); const { data: customers = [], isLoading, isFetching } = useTenantCustomers(); const updateCustomer = useUpdateTenantCustomer(); const [search, setSearch] = useState(''); const debouncedSearch = useDebouncedValue(search); const [activeFilter, setActiveFilter] = useState('all'); const [editRow, setEditRow] = useState(null); const [form, setForm] = useState({ name: '', phone: '', email: '' }); const stats = useMemo(() => { const total = customers.length; return { total, withEmail: customers.filter((row) => Boolean(row.email)).length, withPhone: customers.filter((row) => Boolean(row.phone)).length, totalBookings: customers.reduce((sum, row) => sum + (Number(row.totalbookings) || 0), 0), }; }, [customers]); const rows = useMemo(() => { let list = customers; if (activeFilter === 'withEmail') list = list.filter((row) => Boolean(row.email)); else if (activeFilter === 'withPhone') list = list.filter((row) => Boolean(row.phone)); return list.filter((row) => matchesQuery(row, [custName, custPhone, custEmail], debouncedSearch)); }, [customers, activeFilter, debouncedSearch]); const toggleFilter = (key) => setActiveFilter((current) => (current === key ? 'all' : key)); const openEdit = (row) => { setEditRow(row); setForm({ name: custName(row) === '—' ? '' : custName(row), phone: custPhone(row), email: custEmail(row) }); }; const save = () => { /* The record stores firstname/lastname but the dialog edits one name field. Splitting on the first space keeps the surname through a round trip instead of blanking it. */ const [firstname, ...rest] = String(form.name || '').trim().split(/\s+/); updateCustomer.mutate( { id: custId(editRow), data: { firstname: firstname || '', lastname: rest.join(' '), phone: form.phone || '', email: form.email || '', }, }, { onSuccess: (res) => res?.success !== false && setEditRow(null) } ); }; const columns = useMemo( () => [ { key: 'customer', header: 'Customer', sortable: true, accessor: custName, cell: (row) => (
{custName(row)}
ID #{custId(row) ?? '—'}
{custPhone(row) || '—'}
{custEmail(row) ? ({custEmail(row)}
) : null}