Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions cafe-management-sys/src/components/common/ResponsiveTable.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Table,
Expand All @@ -7,12 +7,14 @@ import {
TableContainer,
TableHead,
TableRow,
TableSortLabel,
Typography,
IconButton,
CircularProgress,
} from '@mui/material';
import ChevronLeft from '@mui/icons-material/ChevronLeft';
import ChevronRight from '@mui/icons-material/ChevronRight';
import { sortRows, toggleSort } from '../../utils/sortable';

const PAGE_SIZE = 25;

Expand All @@ -37,7 +39,13 @@ const ResponsiveTable = ({
emptyMessage = 'No records found',
}) => {
const [page, setPage] = useState(0);
const pageCount = Math.max(1, Math.ceil((rows.length || 0) / PAGE_SIZE));
const [sort, setSort] = useState(null);

const sortableRows = useMemo(
() => (sort ? sortRows(rows, sort.key, sort.dir) : rows),
[rows, sort]
);
const pageCount = Math.max(1, Math.ceil((sortableRows.length || 0) / PAGE_SIZE));

// Reset to the first page whenever the record set changes (search/filter).
useEffect(() => {
Expand All @@ -46,7 +54,7 @@ const ResponsiveTable = ({

const safePage = Math.min(page, pageCount - 1);
const start = safePage * PAGE_SIZE;
const visible = rows.slice(start, start + PAGE_SIZE);
const visible = sortableRows.slice(start, start + PAGE_SIZE);

if (loading) {
return (
Expand All @@ -56,7 +64,7 @@ const ResponsiveTable = ({
);
}

if (!rows.length) {
if (!sortableRows.length) {
return (
<Box sx={{ py: 4 }}>
<Typography variant="body2" color="text.secondary" align="center">
Expand All @@ -75,7 +83,17 @@ const ResponsiveTable = ({
<TableRow>
{columns.map((col, i) => (
<TableCell key={i} align={col.align || 'left'}>
{col.label}
{col.sortKey ? (
<TableSortLabel
active={sort?.key === col.sortKey}
direction={sort?.key === col.sortKey ? sort.dir : 'asc'}
onClick={() => setSort((s) => toggleSort(s, col.sortKey))}
>
{col.label}
</TableSortLabel>
) : (
col.label
)}
</TableCell>
))}
</TableRow>
Expand All @@ -90,7 +108,7 @@ const ResponsiveTable = ({
</Box>

{/* Pagination */}
{rows.length > PAGE_SIZE && (
{sortableRows.length > PAGE_SIZE && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 2, py: 1.5 }}>
<Typography variant="body2" color="text.secondary">
{start + 1}–{Math.min(start + PAGE_SIZE, rows.length)} of {rows.length}
Expand Down
6 changes: 3 additions & 3 deletions cafe-management-sys/src/pages/admin/AdminInvoices.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,12 @@ const AdminInvoices = () => {
loading={loading}
emptyMessage="No invoices"
columns={[
{ label: 'Invoice #' },
{ label: 'Date' },
{ label: 'Invoice #', sortKey: 'invoiceNumber' },
{ label: 'Date', sortKey: 'issuedAt' },
{ label: 'Status' },
{ label: 'Taxable', align: 'right' },
{ label: 'CGST+SGST', align: 'right' },
{ label: 'Total', align: 'right' },
{ label: 'Total', align: 'right', sortKey: 'grandTotal' },
{ label: '', align: 'center' },
]}
renderRow={(inv) => {
Expand Down
6 changes: 3 additions & 3 deletions cafe-management-sys/src/pages/admin/AdminOrders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,12 @@ const AdminOrders = () => {
emptyMessage="No orders found"
columns={[
{ label: '', align: 'center' },
{ label: 'Order #' },
{ label: 'Order #', sortKey: 'orderNumber' },
{ label: 'Customer' },
{ label: 'Items' },
{ label: 'Total' },
{ label: 'Total', sortKey: 'total' },
{ label: 'Status' },
{ label: 'Date' },
{ label: 'Date', sortKey: 'createdAt' },
{ label: '', align: 'center' },
]}
renderRow={(order) => (
Expand Down
27 changes: 27 additions & 0 deletions cafe-management-sys/src/utils/sortable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const isNumeric = (v) => v != null && v !== '' && !Number.isNaN(Number(v));

export const compareValues = (a, b) => {
const an = isNumeric(a);
const bn = isNumeric(b);
if (an && bn) return Number(a) - Number(b);
if (a == null || a === '') return 1; // sort empties last
if (b == null || b === '') return -1;
return String(a).localeCompare(String(b));
};

export const sortRows = (rows = [], key, dir = 'asc') => {
if (!key) return [...rows];
const factor = dir === 'desc' ? -1 : 1;
const isEmpty = (v) => v == null || v === '';
return [...rows].sort((x, y) => {
const a = x?.[key];
const b = y?.[key];
if (isEmpty(a) !== isEmpty(b)) return isEmpty(a) ? 1 : -1; // empties last always
return factor * compareValues(a, b);
});
};

export const toggleSort = (current, key) =>
current && current.key === key && current.dir === 'asc'
? { key, dir: 'desc' }
: { key, dir: 'asc' };
41 changes: 41 additions & 0 deletions cafe-management-sys/src/utils/sortable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { compareValues, sortRows, toggleSort } from './sortable';

describe('compareValues', () => {
it('sorts numbers numerically and strings lexically', () => {
expect(compareValues(2, 10)).toBeLessThan(0);
expect(compareValues('b', 'a')).toBeGreaterThan(0);
});
it('puts empty/missing values last', () => {
expect(compareValues('', 'a')).toBeGreaterThan(0);
expect(compareValues(null, 'a')).toBeGreaterThan(0);
});
});

describe('sortRows', () => {
const rows = [
{ id: 'a', orderNumber: 'ORD-0002', total: 100 },
{ id: 'b', orderNumber: 'ORD-0010', total: 50 },
{ id: 'c', orderNumber: 'ORD-0001', total: null },
];
it('sorts ascending (string-aware, not numeric-mangled)', () => {
expect(sortRows(rows, 'orderNumber', 'asc').map((r) => r.id)).toEqual(['c', 'a', 'b']);
});
it('sorts totals numerically with nulls last', () => {
expect(sortRows(rows, 'total', 'asc').map((r) => r.id)).toEqual(['b', 'a', 'c']);
expect(sortRows(rows, 'total', 'desc').map((r) => r.id)).toEqual(['a', 'b', 'c']);
});
it('returns a copy by default and does not mutate input', () => {
const copy = sortRows(rows, 'total');
expect(copy).not.toBe(rows);
expect(rows[0].id).toBe('a');
});
});

describe('toggleSort', () => {
it('toggles asc->desc and switches keys', () => {
expect(toggleSort({ key: 'total', dir: 'asc' }, 'total')).toEqual({ key: 'total', dir: 'desc' });
expect(toggleSort({ key: 'total', dir: 'desc' }, 'total')).toEqual({ key: 'total', dir: 'asc' });
expect(toggleSort(null, 'date')).toEqual({ key: 'date', dir: 'asc' });
});
});
Loading