import { useEffect, useMemo, useState } from 'react' import { MessageSquare, Pencil, Plus, RotateCcw, Search, Shield, Ticket, Trash2 } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { AddCustomerModal } from '../components/AddCustomerModal' import { Breadcrumbs } from '../components/Breadcrumbs' import { ChangeUserAccessModal } from '../components/ChangeUserAccessModal' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { EditCustomerModal } from '../components/EditCustomerModal' import { Pagination } from '../components/Pagination' import { ToggleSwitch } from '../components/ToggleSwitch' import { Tooltip } from '../components/Tooltip' import { useAuth } from '../context/AuthContext' import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import { ApiError, isAbortError } from '../lib/api' import { getActiveBusinessId } from '../lib/businessContext' import { formatCellForDisplay } from '../lib/cellNumber' import { listCustomers, removeCustomer, updateCustomerEnabled, type BusinessCustomerListItem, type CustomerAccessFilter, type CustomersListResponse, } from '../services/customerService' import { formatIrtPrice } from '../utils/irtPrice' import { textLocaleAttrs } from '../utils/textLocale' import filterStyles from '../components/ListFiltersPanel.module.css' import pageStyles from '../components/PageContent.module.css' import accessStyles from '../components/ChangeUserAccessModal.module.css' import styles from './CustomersPage.module.css' const PAGE_SIZE = 24 const COLUMN_COUNT = 6 function formatDate(value: string, locale: 'en' | 'fa') { const d = new Date(value) if (Number.isNaN(d.getTime())) return value const intlLocale = locale === 'fa' ? 'fa-IR' : 'en-US' return d.toLocaleDateString(intlLocale, { year: 'numeric', month: 'short', day: '2-digit' }) } function displayName(customer: BusinessCustomerListItem) { const name = [customer.firstName, customer.lastName].filter(Boolean).join(' ').trim() return name || '—' } function formatTransactionTotal(total: number | null | undefined) { if (total == null || total === 0) { return } return formatIrtPrice(total) } function accessBadgeKey(user: BusinessCustomerListItem): string | null { if (user.isBusinessOwner) return 'customers.access.badge.owner' if (user.teamRole === 'admin') return 'customers.access.badge.admin' if (user.teamRole === 'editor') return 'customers.access.badge.editor' if (user.teamRole === 'viewer') return 'customers.access.badge.viewer' if (user.businessMemberId) return 'customers.access.badge.manager' return null } export function CustomersPage() { const t = useT() const { locale } = useLocale() const { user: authUser } = useAuth() const { showToast } = useToast() const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [appliedFilters, setAppliedFilters] = useState<{ name?: string cellNumber?: string access: CustomerAccessFilter }>({ access: 'all' }) const [page, setPage] = useState(1) const [draftName, setDraftName] = useState('') const [draftCell, setDraftCell] = useState('') const [draftAccess, setDraftAccess] = useState('all') const [togglingId, setTogglingId] = useState(null) const [editTarget, setEditTarget] = useState(null) const [accessTarget, setAccessTarget] = useState(null) const [createOpen, setCreateOpen] = useState(false) const [removeTarget, setRemoveTarget] = useState(null) const [removing, setRemoving] = useState(false) const membership = useMemo(() => { const activeId = getActiveBusinessId() if (!authUser?.businesses.length) return undefined return ( authUser.businesses.find((b) => String(b.id) === String(activeId)) ?? authUser.businesses[0] ) }, [authUser]) const isSuperAdmin = Boolean( authUser?.isSuperAdmin || authUser?.roles.includes('super_admin'), ) const canManageAccess = isSuperAdmin || Boolean(membership?.isOwner) || Boolean(membership?.permissions.includes('business.team.update')) function accessChangeDisabledReason(customer: BusinessCustomerListItem): string | null { if (customer.isBusinessOwner) return t('customers.access.ownerLocked') if (customer.teamRole === 'admin' && !isSuperAdmin) { return t('customers.access.adminLocked') } return null } useEffect(() => { const controller = new AbortController() async function load() { setLoading(true) setError('') try { const result = await listCustomers( { page, pageSize: PAGE_SIZE, ...appliedFilters, }, controller.signal, ) if (controller.signal.aborted) return setData(result) } catch (err) { if (isAbortError(err) || controller.signal.aborted) return setError(err instanceof ApiError ? err.message : t('customers.error.load')) } finally { if (!controller.signal.aborted) setLoading(false) } } void load() return () => { controller.abort() } }, [page, appliedFilters.name, appliedFilters.cellNumber, appliedFilters.access, t]) const totalPages = useMemo(() => { const total = data?.total ?? 0 return Math.max(1, Math.ceil(total / PAGE_SIZE)) }, [data?.total]) const showingFrom = useMemo(() => { if (!data || data.total === 0) return 0 return (page - 1) * PAGE_SIZE + 1 }, [data, page]) const showingTo = useMemo(() => { if (!data) return 0 return Math.min(data.total, page * PAGE_SIZE) }, [data, page]) function applyFilters() { setPage(1) setAppliedFilters({ access: draftAccess, ...(draftName.trim() ? { name: draftName.trim() } : {}), ...(draftCell.trim() ? { cellNumber: draftCell.trim() } : {}), }) } function clearFilters() { setDraftName('') setDraftCell('') setDraftAccess('all') setPage(1) setAppliedFilters({ access: 'all' }) } function handleAccessFilterChange(value: CustomerAccessFilter) { setDraftAccess(value) setPage(1) setAppliedFilters({ access: value, ...(draftName.trim() ? { name: draftName.trim() } : {}), ...(draftCell.trim() ? { cellNumber: draftCell.trim() } : {}), }) } async function handleToggleEnabled(customer: BusinessCustomerListItem, isEnabled: boolean) { setTogglingId(customer.id) setError('') try { await updateCustomerEnabled(customer.id, isEnabled) setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => item.id === customer.id ? { ...item, isEnabled } : item, ), } }) showToast( isEnabled ? t('customers.toast.enabled', { name: displayName(customer) }) : t('customers.toast.disabled', { name: displayName(customer) }), 'success', ) } catch (err) { setError(err instanceof ApiError ? err.message : t('customers.error.update')) } finally { setTogglingId(null) } } async function confirmRemove() { if (!removeTarget) return setRemoving(true) setError('') try { await removeCustomer(removeTarget.id) setData((prev) => { if (!prev) return prev return { ...prev, total: Math.max(0, prev.total - 1), items: prev.items.filter((item) => item.id !== removeTarget.id), } }) showToast(t('customers.toast.removed', { name: displayName(removeTarget) }), 'success') setRemoveTarget(null) } catch (err) { setError(err instanceof ApiError ? err.message : t('customers.error.remove')) } finally { setRemoving(false) } } function handleSendSms(customer: BusinessCustomerListItem) { showToast( t('customers.toast.smsSoon', { phone: formatCellForDisplay(customer.cellNumber) }), 'info', ) } function handleTickets(customer: BusinessCustomerListItem) { showToast(t('customers.toast.ticketsSoon', { name: displayName(customer) }), 'info') } function handleCustomerSaved(updated: BusinessCustomerListItem) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => (item.id === updated.id ? updated : item)), } }) showToast(t('customers.toast.updated', { name: displayName(updated) }), 'success') } function handleAccessSaved(updated: BusinessCustomerListItem) { setData((prev) => { if (!prev) return prev return { ...prev, items: prev.items.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)), } }) const roleLabel = updated.isBusinessOwner ? t('customers.access.badge.owner') : updated.teamRole ? t(`customers.access.role.${updated.teamRole}`) : t('customers.access.customer') showToast(t('customers.toast.accessUpdated', { role: roleLabel }), 'success') } function handleCustomerCreated(customer: BusinessCustomerListItem) { setPage(1) setData((prev) => { if (!prev) { return { items: [customer], total: 1, page: 1, pageSize: PAGE_SIZE, } } return { ...prev, total: prev.total + 1, page: 1, items: [customer, ...prev.items.filter((item) => item.id !== customer.id)].slice( 0, PAGE_SIZE, ), } }) showToast(t('customers.toast.added', { name: displayName(customer) }), 'success') } return (

{t('customers.title')}

{ e.preventDefault(); applyFilters() }}>
setDraftName(e.target.value)} placeholder={t('customers.filter.name')} aria-label={t('customers.filter.name')} />
setDraftCell(e.target.value)} placeholder={t('customers.filter.cell')} aria-label={t('customers.filter.cell')} autoComplete="off" />
{t('customers.listTitle')}
{data ? ( data.total > 0 ? ( t('customers.showing', { from: showingFrom, to: showingTo, total: data.total, }) ) : ( t('customers.none') ) ) : ( ' ' )}
{error &&
{error}
} {loading && ( )} {!loading && data?.items?.length === 0 && ( )} {!loading && data?.items?.map((customer) => { const name = displayName(customer) const nameLocale = textLocaleAttrs(name) const badgeKey = accessBadgeKey(customer) const accessLocked = accessChangeDisabledReason(customer) return ( ) })}
{t('customers.col.name')} {t('customers.col.cell')} {t('customers.col.orders')} {t('customers.col.transactions')} {t('customers.col.date')} {t('customers.col.actions')}
{t('customers.loading')}
{t('customers.empty')}
{name}
{badgeKey ? ( {t(badgeKey)} ) : null} {!customer.isEnabled && (
{t('customers.disabled')}
)}
{formatCellForDisplay(customer.cellNumber)} {customer.orderCount == null || customer.orderCount === 0 ? ( {t('customers.noOrders')} ) : ( customer.orderCount )} {formatTransactionTotal(customer.totalTransactionsIrt)} {formatDate(customer.createdAt, locale)}
void handleToggleEnabled(customer, isEnabled) } /> {canManageAccess ? ( ) : null}
{t('customers.pageMeta', { page, totalPages, pageSize: PAGE_SIZE, total: data?.total ?? 0, })}
setCreateOpen(false)} onCreated={handleCustomerCreated} /> setEditTarget(null)} onSaved={handleCustomerSaved} /> setAccessTarget(null)} onSaved={handleAccessSaved} /> setRemoveTarget(null)} onConfirm={() => void confirmRemove()} />
) }