updatess on the ui changs

This commit is contained in:
2026-08-18 12:46:50 +05:30
parent 8d154f06d9
commit e420223622
24 changed files with 6640 additions and 1797 deletions

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
@@ -11,12 +11,31 @@ import {
MdOutlineMailOutline,
MdOutlinePhoneAndroid,
MdPersonAddAlt1,
MdOutlineReceiptLong
MdOutlineReceiptLong,
MdArrowUpward,
MdArrowDownward,
MdUnfoldMore
} from 'react-icons/md';
// TanStack Table v9 — same engine orders.js/riders.js/Tenants.js use, so
// sorting/resizing/pagination behave identically across the console.
import {
useTable,
flexRender,
tableFeatures,
rowSortingFeature,
createSortedRowModel,
sortFns,
columnSizingFeature,
columnResizingFeature,
rowPaginationFeature,
createPaginatedRowModel
} from '@tanstack/react-table';
import { Grid } from '@astryxdesign/core/Grid';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Center } from '@astryxdesign/core/Center';
import { Text } from '@astryxdesign/core/Text';
import { Button } from '@astryxdesign/core/Button';
import { IconButton } from '@astryxdesign/core/IconButton';
@@ -26,6 +45,7 @@ import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Divider } from '@astryxdesign/core/Divider';
import { EmptyState } from '@astryxdesign/core/EmptyState';
import { Pagination } from '@astryxdesign/core/Pagination';
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
import Loader from 'components/Loader';
@@ -64,6 +84,29 @@ const custPhone = (row) => row.phone || '—';
const custEmail = (row) => row.email || '—';
const custBookings = (row) => row.totalbookings ?? 0;
// TanStack Table v9 registers features explicitly — only sorting/resizing/
// pagination ship here (no row-selection or column-visibility, since this
// table has no bulk-select or column-hide flow).
const CUSTOMERS_TABLE_FEATURES = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
columnSizingFeature,
columnResizingFeature,
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel()
});
const CUSTOMERS_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
// Uppercase micro-label for the table's column headings — same convention
// orders.js's/riders.js's/Tenants.js's tables use.
const SectionLabel = ({ children }) => (
<Text type="label" weight="bold" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
{children}
</Text>
);
const Customers = () => {
const isMobile = useIsNarrowViewport();
const navigate = useNavigate();
@@ -75,6 +118,9 @@ const Customers = () => {
const [addressInput, setAddressInput] = useState('');
const [pickAddress, setPickAddress] = useState({});
const [latLng, setLatLng] = useState({ latitude: '', longitude: '' });
const [sorting, setSorting] = useState([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const [columnSizing, setColumnSizing] = useState({});
const { data: customers = [], isLoading } = useQuery({ queryKey: ['admin-customers'], queryFn: getAdminCustomers });
@@ -158,6 +204,132 @@ const Customers = () => {
setAddressInput(place.formatted_address);
};
// A search re-scopes the list, so page 1 is the only page that still
// means anything — same reset orders.js/riders.js apply.
useEffect(() => {
setPagination((p) => ({ ...p, pageIndex: 0 }));
}, [debouncedSearch]);
// The column model — everything a column IS declared once here instead of
// a hardcoded header row + hand-written body row, same shape as orders.js's/
// riders.js's/Tenants.js's `columns`. Not wrapped in useMemo: the Actions
// column's cell calls `openEdit`, which is cheap to recompute per render
// and safe here since it closes only over stable setState functions.
const columns = [
{
id: 'serial',
size: 44,
minSize: 36,
maxSize: 52,
enableSorting: false,
enableResizing: false,
meta: { label: '#' },
header: () => <SectionLabel>#</SectionLabel>,
cell: ({ table: t, row }) => (
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
{String(pagination.pageIndex * pagination.pageSize + t.getRowModel().rows.indexOf(row) + 1).padStart(2, '0')}
</Text>
)
},
{
id: 'customer',
accessorFn: (row) => custName(row),
size: 260,
minSize: 200,
meta: { label: 'Customer' },
header: () => <SectionLabel>Customer</SectionLabel>,
cell: ({ getValue, row }) => (
<HStack gap={2} vAlign="center">
<AccentAvatar color={BRAND} size={32}>
<MdPeopleAlt size={16} />
</AccentAvatar>
<VStack gap={0} padding={0}>
<Text weight="bold">{getValue()}</Text>
<Text type="supporting" color="secondary">
ID #{custId(row.original) ?? '—'}
</Text>
</VStack>
</HStack>
)
},
{
id: 'contact',
accessorFn: (row) => custPhone(row),
size: 240,
minSize: 180,
meta: { label: 'Contact' },
header: () => <SectionLabel>Contact</SectionLabel>,
cell: ({ getValue, row }) => (
<VStack gap={0} padding={0}>
<HStack gap={1} vAlign="center">
<MdPhone size={12} color={DT.textMuted} />
<Text weight="bold">{getValue()}</Text>
</HStack>
{row.original.email && (
<HStack gap={1} vAlign="center">
<MdMail size={12} color={DT.textMuted} />
<Text type="supporting" color="secondary">
{row.original.email}
</Text>
</HStack>
)}
</VStack>
)
},
{
id: 'bookings',
accessorFn: (row) => custBookings(row),
size: 130,
minSize: 100,
meta: { label: 'Bookings' },
header: () => <SectionLabel>Bookings</SectionLabel>,
cell: ({ getValue }) => <Text hasTabularNumbers>{getValue()}</Text>
},
{
id: 'actions',
size: 110,
minSize: 90,
enableSorting: false,
enableResizing: false,
meta: { label: 'Actions', align: 'right' },
header: () => <SectionLabel>Actions</SectionLabel>,
cell: ({ row }) => (
<HStack justify="end">
<IconButton
label="Edit customer"
tooltip="Edit customer"
size="sm"
variant="ghost"
icon={<MdEdit size={16} />}
onClick={() => openEdit(row.original)}
/>
</HStack>
)
}
];
const table = useTable({
features: CUSTOMERS_TABLE_FEATURES,
// `rows` is already useMemo'd above (stable reference across renders) —
// required for TanStack's row-model memoization to hold and for
// pagination clicks to stick (an unmemoized array reads as "data
// changed" on every render and auto-resets to page 1).
data: rows,
columns,
getRowId: (row, index) => String(custId(row) ?? `row-${index}`),
columnResizeMode: 'onChange',
state: { sorting, pagination, columnSizing },
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnSizingChange: setColumnSizing
});
// Skeleton rows and the empty state span exactly the columns that are
// actually rendered. Read off `columns.length` directly (not
// `table.getVisibleLeafColumns()`): that method only exists when
// columnVisibilityFeature is registered, which this table doesn't need.
const colCount = columns.length;
return (
<>
{isLoading && <Loader />}
@@ -224,62 +396,150 @@ const Customers = () => {
/>
) : (
<MobileCardList scroll>
{rows.map((row, index) => (
<MobileCard
key={custId(row) || index}
accent={BRAND}
header={
<HStack justify="between" vAlign="center" gap={1}>
<HStack gap={2} vAlign="center" minWidth={0}>
<AccentAvatar color={BRAND} size={36}>
<MdPeopleAlt size={18} />
</AccentAvatar>
<VStack gap={0} padding={0} minWidth={0}>
<Text weight="bold" maxLines={1}>
{custName(row)}
</Text>
<Text type="supporting" color="secondary">
ID #{custId(row) ?? '—'}
</Text>
</VStack>
{table.getRowModel().rows.map((tableRow) => {
const row = tableRow.original;
return (
<MobileCard
key={tableRow.id}
accent={BRAND}
header={
<HStack justify="between" vAlign="center" gap={1}>
<HStack gap={2} vAlign="center" minWidth={0}>
<AccentAvatar color={BRAND} size={36}>
<MdPeopleAlt size={18} />
</AccentAvatar>
<VStack gap={0} padding={0} minWidth={0}>
<Text weight="bold" maxLines={1}>
{custName(row)}
</Text>
<Text type="supporting" color="secondary">
ID #{custId(row) ?? '—'}
</Text>
</VStack>
</HStack>
<IconButton
label="Edit customer"
tooltip="Edit customer"
size="sm"
variant="ghost"
icon={<MdEdit size={14} />}
onClick={() => openEdit(row)}
/>
</HStack>
<IconButton
label="Edit customer"
tooltip="Edit customer"
size="sm"
variant="ghost"
icon={<MdEdit size={14} />}
onClick={() => openEdit(row)}
/>
</HStack>
}
>
<MobileFieldGrid>
<MobileField label="Phone" value={custPhone(row)} />
<MobileField label="Email" value={custEmail(row)} />
<MobileField label="Bookings" value={custBookings(row)} />
</MobileFieldGrid>
</MobileCard>
))}
}
>
<MobileFieldGrid>
<MobileField label="Phone" value={custPhone(row)} />
<MobileField label="Email" value={custEmail(row)} />
<MobileField label="Bookings" value={custBookings(row)} />
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)
) : (
<TableScroll minWidth={720}>
<Table density="balanced" dividers="rows" hasHover>
<TableScroll minWidth={table.getTotalSize()}>
{/* table-layout: fixed makes the declared column widths
authoritative — same reasoning as orders.js's/riders.js's/
Tenants.js's tables. Header and body are both rendered FROM
the column model, so they cannot drift apart. */}
<Table
density="balanced"
dividers="rows"
hasHover
verticalAlign="middle"
aria-label="Customers"
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
>
<TableHeader>
<TableRow>
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
<TableHeaderCell>Customer</TableHeaderCell>
<TableHeaderCell>Contact</TableHeaderCell>
<TableHeaderCell>Bookings</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} isHeaderRow>
{headerGroup.headers.map((header) => {
const align = header.column.columnDef.meta?.align;
const canSort = header.column.getCanSort();
const sorted = header.column.getIsSorted();
return (
<TableHeaderCell
key={header.id}
scope="col"
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
style={{
width: header.getSize(),
minWidth: header.column.columnDef.minSize,
maxWidth: header.column.columnDef.maxSize || header.getSize(),
textAlign: align,
position: 'relative'
}}
>
<HStack
gap={1}
vAlign="center"
justify={align === 'right' ? 'end' : align === 'center' ? 'center' : 'start'}
role={canSort ? 'button' : undefined}
tabIndex={canSort ? 0 : undefined}
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
onKeyDown={
canSort
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
header.column.toggleSorting();
}
}
: undefined
}
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{canSort &&
(sorted === 'asc' ? (
<MdArrowUpward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
) : sorted === 'desc' ? (
<MdArrowDownward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
) : (
<MdUnfoldMore size={12} color={DT.textMuted} style={{ flexShrink: 0, opacity: 0.6 }} />
))}
</HStack>
{header.column.getCanResize() && (
<Center
aria-hidden="true"
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
style={{
position: 'absolute',
top: 0,
right: 0,
height: '100%',
width: 10,
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none'
}}
>
<Center
style={{
width: 2,
height: '60%',
borderRadius: 'var(--radius-element)',
backgroundColor: header.column.getIsResizing() ? DT.brand : DT.borderSubtle,
transition: 'background-color 120ms ease'
}}
/>
</Center>
)}
</TableHeaderCell>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading && <TableLoader rows={5} columns={5} />}
{isLoading && <TableLoader rows={5} columns={colCount} />}
{rows.length === 0 && !isLoading ? (
<TableRow>
<TableCell colSpan={5}>
<TableCell colSpan={colCount}>
<EmptyState
title="No customers to show"
description="Customers appear here once they've placed an order."
@@ -288,55 +548,19 @@ const Customers = () => {
</TableCell>
</TableRow>
) : (
rows.map((row, index) => (
<TableRow key={custId(row) || index}>
<TableCell>
<Text type="supporting" weight="bold" color="secondary">
{String(index + 1).padStart(2, '0')}
</Text>
</TableCell>
<TableCell>
<HStack gap={2} vAlign="center">
<AccentAvatar color={BRAND} size={32}>
<MdPeopleAlt size={16} />
</AccentAvatar>
<VStack gap={0} padding={0}>
<Text weight="bold">{custName(row)}</Text>
<Text type="supporting" color="secondary">
ID #{custId(row) ?? '—'}
</Text>
</VStack>
</HStack>
</TableCell>
<TableCell>
<VStack gap={0} padding={0}>
<HStack gap={1} vAlign="center">
<MdPhone size={12} color={DT.textMuted} />
<Text weight="bold">{custPhone(row)}</Text>
</HStack>
{row.email && (
<HStack gap={1} vAlign="center">
<MdMail size={12} color={DT.textMuted} />
<Text type="supporting" color="secondary">
{row.email}
</Text>
</HStack>
)}
</VStack>
</TableCell>
<TableCell>{custBookings(row)}</TableCell>
<TableCell>
<HStack justify="end">
<IconButton
label="Edit customer"
tooltip="Edit customer"
size="sm"
variant="ghost"
icon={<MdEdit size={16} />}
onClick={() => openEdit(row)}
/>
</HStack>
</TableCell>
table.getRowModel().rows.map((tableRow) => (
<TableRow key={tableRow.id}>
{tableRow.getAllCells().map((cell) => (
<TableCell
key={cell.id}
style={{
textAlign: cell.column.columnDef.meta?.align,
maxWidth: cell.column.columnDef.maxSize || cell.column.getSize()
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
@@ -344,6 +568,30 @@ const Customers = () => {
</Table>
</TableScroll>
)}
{rows.length > 0 && (
<>
<Divider />
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" paddingInline={4} paddingBlock={2}>
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
{`${pagination.pageIndex * pagination.pageSize + 1}–${Math.min(
(pagination.pageIndex + 1) * pagination.pageSize,
rows.length
)} of ${rows.length} customer${rows.length === 1 ? '' : 's'}`}
</Text>
<Pagination
page={pagination.pageIndex + 1}
onChange={(nextPage) => table.setPageIndex(nextPage - 1)}
totalItems={rows.length}
pageSize={pagination.pageSize}
pageSizeOptions={CUSTOMERS_PAGE_SIZE_OPTIONS}
onPageSizeChange={(size) => table.setPageSize(size)}
size="sm"
label="Customers pagination"
/>
</HStack>
</>
)}
</DataCard>
</PageShell>