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