diff --git a/cafe-management-sys/src/components/common/ResponsiveTable.jsx b/cafe-management-sys/src/components/common/ResponsiveTable.jsx index 817616f..ea8890c 100644 --- a/cafe-management-sys/src/components/common/ResponsiveTable.jsx +++ b/cafe-management-sys/src/components/common/ResponsiveTable.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Box, Table, @@ -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; @@ -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(() => { @@ -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 ( @@ -56,7 +64,7 @@ const ResponsiveTable = ({ ); } - if (!rows.length) { + if (!sortableRows.length) { return ( @@ -75,7 +83,17 @@ const ResponsiveTable = ({ {columns.map((col, i) => ( - {col.label} + {col.sortKey ? ( + setSort((s) => toggleSort(s, col.sortKey))} + > + {col.label} + + ) : ( + col.label + )} ))} @@ -90,7 +108,7 @@ const ResponsiveTable = ({ {/* Pagination */} - {rows.length > PAGE_SIZE && ( + {sortableRows.length > PAGE_SIZE && ( {start + 1}–{Math.min(start + PAGE_SIZE, rows.length)} of {rows.length} diff --git a/cafe-management-sys/src/pages/admin/AdminInvoices.jsx b/cafe-management-sys/src/pages/admin/AdminInvoices.jsx index 135a045..c1f5ccf 100644 --- a/cafe-management-sys/src/pages/admin/AdminInvoices.jsx +++ b/cafe-management-sys/src/pages/admin/AdminInvoices.jsx @@ -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) => { diff --git a/cafe-management-sys/src/pages/admin/AdminOrders.jsx b/cafe-management-sys/src/pages/admin/AdminOrders.jsx index cf7be1b..4bf6b79 100644 --- a/cafe-management-sys/src/pages/admin/AdminOrders.jsx +++ b/cafe-management-sys/src/pages/admin/AdminOrders.jsx @@ -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) => ( diff --git a/cafe-management-sys/src/utils/sortable.js b/cafe-management-sys/src/utils/sortable.js new file mode 100644 index 0000000..380e825 --- /dev/null +++ b/cafe-management-sys/src/utils/sortable.js @@ -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' }; \ No newline at end of file diff --git a/cafe-management-sys/src/utils/sortable.test.js b/cafe-management-sys/src/utils/sortable.test.js new file mode 100644 index 0000000..65c9f20 --- /dev/null +++ b/cafe-management-sys/src/utils/sortable.test.js @@ -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' }); + }); +}); \ No newline at end of file