@evenicanpm/portal-table-ui 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/auth/auth-context.tsx +92 -0
- package/src/features/table/components/dashboard/table/bulk-action-button.tsx +77 -0
- package/src/features/table/components/dashboard/table/data-mask-map.ts +26 -0
- package/src/features/table/components/dashboard/table/details/details.tsx +185 -0
- package/src/features/table/components/dashboard/table/details/label.tsx +26 -0
- package/src/features/table/components/dashboard/table/details/rows/downloads.tsx +25 -0
- package/src/features/table/components/dashboard/table/details/rows/primitive.tsx +25 -0
- package/src/features/table/components/dashboard/table/details/rows/table.tsx +73 -0
- package/{dist/features/table/components/dashboard/table/details/styles.d.ts → src/features/table/components/dashboard/table/details/styles.ts} +10 -2
- package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +407 -0
- package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +36 -0
- package/src/features/table/components/dashboard/table/filter-render-map.tsx +10 -0
- package/src/features/table/components/dashboard/table/filters/date-filter.tsx +31 -0
- package/src/features/table/components/dashboard/table/filters/status-filter.tsx +36 -0
- package/src/features/table/components/dashboard/table/no-results.tsx +26 -0
- package/src/features/table/components/dashboard/table/status-pill.tsx +18 -0
- package/src/features/table/components/dashboard/table/table-header.tsx +98 -0
- package/src/features/table/components/dashboard/table/table-input.tsx +94 -0
- package/src/features/table/components/dashboard/table/table-row/action-button.tsx +47 -0
- package/src/features/table/components/dashboard/table/table-row/table-row.tsx +144 -0
- package/src/features/table/components/dashboard/table/table-row-skeleton.tsx +35 -0
- package/src/features/table/components/dashboard/table/table.tsx +361 -0
- package/src/features/table/components/dashboard/table/use-select-rows.ts +53 -0
- package/src/features/table/components/dashboard/table-dashboard.tsx +71 -0
- package/{dist/features/table/index.d.ts → src/features/table/index.ts} +7 -1
- package/src/features/table/logic/resolvers.ts +111 -0
- package/src/features/table/logic/transformers.ts +56 -0
- package/src/features/table/logic/types.ts +8 -0
- package/{dist/features/table/types/handlers.d.ts → src/features/table/types/handlers.ts} +30 -12
- package/{dist/features/table/types/index.d.ts → src/features/table/types/index.ts} +1 -0
- package/{dist/features/table/types/queries.d.ts → src/features/table/types/queries.ts} +10 -3
- package/src/features/table/types/schemas.ts +89 -0
- package/{dist/index.d.ts → src/index.ts} +2 -1
- package/src/portal.tsx +68 -0
- package/dist/auth/auth-context.d.ts +0 -36
- package/dist/features/settings/index.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/actions.d.ts +0 -14
- package/dist/features/table/components/dashboard/table/data-mask-map.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/details/details.d.ts +0 -27
- package/dist/features/table/components/dashboard/table/details/label.d.ts +0 -5
- package/dist/features/table/components/dashboard/table/details/rows/downloads.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/details/rows/primitive.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/details/rows/table.d.ts +0 -17
- package/dist/features/table/components/dashboard/table/filter-render-map.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/filters/date-filter.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/filters/status-filter.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/no-results.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/status-pill.d.ts +0 -8
- package/dist/features/table/components/dashboard/table/table-header.d.ts +0 -16
- package/dist/features/table/components/dashboard/table/table-input.d.ts +0 -11
- package/dist/features/table/components/dashboard/table/table-row/action-button.d.ts +0 -8
- package/dist/features/table/components/dashboard/table/table-row/table-row.d.ts +0 -32
- package/dist/features/table/components/dashboard/table/table-row-skeleton.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/table.d.ts +0 -18
- package/dist/features/table/components/dashboard/table-dashboard.d.ts +0 -22
- package/dist/features/table/logic/resolvers.d.ts +0 -21
- package/dist/features/table/logic/transformers.d.ts +0 -35
- package/dist/features/table/logic/types.d.ts +0 -8
- package/dist/features/table/types/schemas.d.ts +0 -31
- package/dist/portal.d.ts +0 -38
- package/dist/tsconfig.tsbuildinfo +0 -1
- /package/{dist/features/table/components/dashboard/index.d.ts → src/features/table/components/dashboard/index.tsx} +0 -0
- /package/{dist/features/table/components/dashboard/table/details/index.d.ts → src/features/table/components/dashboard/table/details/index.ts} +0 -0
- /package/{dist/features/table/components/dashboard/table/details/rows/index.d.ts → src/features/table/components/dashboard/table/details/rows/index.ts} +0 -0
- /package/{dist/features/table/components/dashboard/table/index.d.ts → src/features/table/components/dashboard/table/index.ts} +0 -0
- /package/{dist/features/table/logic/index.d.ts → src/features/table/logic/index.ts} +0 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
Divider,
|
|
7
|
+
Skeleton,
|
|
8
|
+
Typography,
|
|
9
|
+
} from "@mui/material";
|
|
10
|
+
import Grid2 from "@mui/material/Grid2";
|
|
11
|
+
import { StatusPill } from "../../status-pill";
|
|
12
|
+
import type { DetailsTemplateProps } from "./template-registry";
|
|
13
|
+
|
|
14
|
+
// Formatting utilities
|
|
15
|
+
const formatCurrency = (amount?: number): string => {
|
|
16
|
+
if (amount === undefined || amount === null) return "$0.00";
|
|
17
|
+
return new Intl.NumberFormat("en-US", {
|
|
18
|
+
style: "currency",
|
|
19
|
+
currency: "USD",
|
|
20
|
+
minimumFractionDigits: 2,
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
}).format(amount);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const formatDate = (dateString?: string): string => {
|
|
26
|
+
if (!dateString) return "—";
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const date = new Date(dateString);
|
|
30
|
+
// Check if date is valid
|
|
31
|
+
if (isNaN(date.getTime())) return dateString;
|
|
32
|
+
|
|
33
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
34
|
+
year: "numeric",
|
|
35
|
+
month: "long",
|
|
36
|
+
day: "numeric",
|
|
37
|
+
}).format(date);
|
|
38
|
+
} catch {
|
|
39
|
+
return dateString;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface InvoiceLine {
|
|
44
|
+
Description?: string;
|
|
45
|
+
ItemId?: string;
|
|
46
|
+
Quantity?: number;
|
|
47
|
+
Price?: number;
|
|
48
|
+
LineAmount?: number;
|
|
49
|
+
UnitOfMeasure?: string;
|
|
50
|
+
FullDescription?: string;
|
|
51
|
+
Savings?: number;
|
|
52
|
+
IsOnSale?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface InvoiceData {
|
|
56
|
+
Status?: string | number;
|
|
57
|
+
SalesInvoiceLine?: InvoiceLine[];
|
|
58
|
+
InvoiceSubtotal?: number;
|
|
59
|
+
TotalTaxAmount?: number;
|
|
60
|
+
Amount?: number;
|
|
61
|
+
CompanyName?: string;
|
|
62
|
+
CompanyEmail?: string;
|
|
63
|
+
CompanyAddress?: string;
|
|
64
|
+
Name?: string;
|
|
65
|
+
Email?: string;
|
|
66
|
+
BillingAddress?: string;
|
|
67
|
+
InvoiceDate?: string;
|
|
68
|
+
DueDate?: string;
|
|
69
|
+
Notes?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isDataEmpty = (data: InvoiceData | null | undefined): boolean => {
|
|
73
|
+
if (!data) return true;
|
|
74
|
+
|
|
75
|
+
const hasNoLineItems =
|
|
76
|
+
!data.SalesInvoiceLine || data.SalesInvoiceLine.length === 0;
|
|
77
|
+
const hasNoBasicInfo = !data.CompanyName && !data.Name && !data.InvoiceDate;
|
|
78
|
+
const hasNoAmounts = !data.Amount && data.Amount !== 0;
|
|
79
|
+
|
|
80
|
+
return hasNoLineItems && hasNoBasicInfo && hasNoAmounts;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const InvoiceDetailsTemplate = ({
|
|
84
|
+
data,
|
|
85
|
+
statusColorMap,
|
|
86
|
+
statusTextMap,
|
|
87
|
+
loading = false,
|
|
88
|
+
}: DetailsTemplateProps) => {
|
|
89
|
+
const invoice = data as InvoiceData;
|
|
90
|
+
|
|
91
|
+
if (!loading && (!invoice || isDataEmpty(invoice))) {
|
|
92
|
+
return (
|
|
93
|
+
<Box sx={{ p: 3, width: "75%", mx: "auto" }}>
|
|
94
|
+
<Card sx={{ p: 6, borderRadius: 3, textAlign: "center" }}>
|
|
95
|
+
<Typography variant="h6" color="text.secondary" gutterBottom>
|
|
96
|
+
No Data Available
|
|
97
|
+
</Typography>
|
|
98
|
+
<Typography variant="body2" color="text.secondary">
|
|
99
|
+
This invoice template has no data to display.
|
|
100
|
+
</Typography>
|
|
101
|
+
</Card>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const items = invoice?.SalesInvoiceLine ?? [];
|
|
107
|
+
const subtotal = invoice?.InvoiceSubtotal ?? 0;
|
|
108
|
+
const tax = invoice?.TotalTaxAmount ?? 0;
|
|
109
|
+
const total = invoice?.Amount ?? 0;
|
|
110
|
+
|
|
111
|
+
const columns = [
|
|
112
|
+
{ label: "#", size: 1 },
|
|
113
|
+
{ label: "Product", size: 3 },
|
|
114
|
+
{ label: "Qty", size: 1 },
|
|
115
|
+
{ label: "Unit", size: 2 },
|
|
116
|
+
{ label: "Unit Price", size: 1.5 },
|
|
117
|
+
{ label: "Savings", size: 1.5 },
|
|
118
|
+
{ label: "Line Total", size: 1.5 },
|
|
119
|
+
{ label: "", size: 0.5 },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
if (loading) {
|
|
123
|
+
return (
|
|
124
|
+
<Box sx={{ p: 3, width: "75%", mx: "auto" }}>
|
|
125
|
+
<Box display="flex" justifyContent="space-between" mb={3}>
|
|
126
|
+
<Skeleton variant="rectangular" width={100} height={32} />
|
|
127
|
+
<Skeleton variant="rectangular" width={120} height={40} />
|
|
128
|
+
</Box>
|
|
129
|
+
<Card sx={{ p: 4, borderRadius: 3 }}>
|
|
130
|
+
<Grid2 container spacing={4}>
|
|
131
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
132
|
+
<Grid2 size={6} key={i}>
|
|
133
|
+
<Skeleton width="40%" height={20} />
|
|
134
|
+
<Skeleton width="80%" />
|
|
135
|
+
<Skeleton width="60%" />
|
|
136
|
+
</Grid2>
|
|
137
|
+
))}
|
|
138
|
+
</Grid2>
|
|
139
|
+
<Divider sx={{ my: 3 }} />
|
|
140
|
+
<Grid2 container sx={{ mb: 2 }}>
|
|
141
|
+
{columns.map((col, i) => (
|
|
142
|
+
<Grid2 key={i} size={col.size}>
|
|
143
|
+
<Skeleton width="60%" />
|
|
144
|
+
</Grid2>
|
|
145
|
+
))}
|
|
146
|
+
</Grid2>
|
|
147
|
+
{Array.from({ length: 4 }).map((_, idx) => (
|
|
148
|
+
<Grid2 container key={idx} sx={{ py: 1.5 }}>
|
|
149
|
+
{columns.map((col, i) => (
|
|
150
|
+
<Grid2 key={i} size={col.size}>
|
|
151
|
+
<Skeleton width="80%" />
|
|
152
|
+
</Grid2>
|
|
153
|
+
))}
|
|
154
|
+
</Grid2>
|
|
155
|
+
))}
|
|
156
|
+
<Box mt={3} display="flex" justifyContent="flex-end">
|
|
157
|
+
<Box width={300}>
|
|
158
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
159
|
+
<Box
|
|
160
|
+
key={i}
|
|
161
|
+
display="flex"
|
|
162
|
+
justifyContent="space-between"
|
|
163
|
+
mb={1}
|
|
164
|
+
>
|
|
165
|
+
<Skeleton width="40%" />
|
|
166
|
+
<Skeleton width="30%" />
|
|
167
|
+
</Box>
|
|
168
|
+
))}
|
|
169
|
+
</Box>
|
|
170
|
+
</Box>
|
|
171
|
+
</Card>
|
|
172
|
+
</Box>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Box sx={{ p: 3, width: "75%", mx: "auto" }}>
|
|
178
|
+
{/* Header */}
|
|
179
|
+
<Box
|
|
180
|
+
display="flex"
|
|
181
|
+
justifyContent="space-between"
|
|
182
|
+
alignItems="center"
|
|
183
|
+
mb={3}
|
|
184
|
+
>
|
|
185
|
+
{invoice?.Status !== undefined && (
|
|
186
|
+
<StatusPill
|
|
187
|
+
status={invoice.Status}
|
|
188
|
+
statusColorMap={statusColorMap}
|
|
189
|
+
statusTextMap={statusTextMap}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
{invoice?.Status !== 3 && invoice?.Status !== undefined && (
|
|
193
|
+
<Button variant="contained" color="warning">
|
|
194
|
+
Pay Now
|
|
195
|
+
</Button>
|
|
196
|
+
)}
|
|
197
|
+
</Box>
|
|
198
|
+
|
|
199
|
+
{/* Notes Section */}
|
|
200
|
+
{invoice?.Notes && (
|
|
201
|
+
<Card sx={{ p: 3, borderRadius: 3, mb: 3 }}>
|
|
202
|
+
<Typography variant="subtitle1" color="text.secondary" mb={1}>
|
|
203
|
+
Notes
|
|
204
|
+
</Typography>
|
|
205
|
+
<Typography
|
|
206
|
+
variant="body2"
|
|
207
|
+
color="text.secondary"
|
|
208
|
+
whiteSpace="pre-line"
|
|
209
|
+
>
|
|
210
|
+
{invoice.Notes}
|
|
211
|
+
</Typography>
|
|
212
|
+
</Card>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Main Invoice Card */}
|
|
216
|
+
<Card sx={{ p: 3, borderRadius: 3 }}>
|
|
217
|
+
{/* Details Section */}
|
|
218
|
+
{(invoice?.CompanyName ||
|
|
219
|
+
invoice?.Name ||
|
|
220
|
+
invoice?.InvoiceDate ||
|
|
221
|
+
invoice?.DueDate) && (
|
|
222
|
+
<>
|
|
223
|
+
<Typography variant="subtitle1" color="text.secondary" mb={2}>
|
|
224
|
+
Details
|
|
225
|
+
</Typography>
|
|
226
|
+
|
|
227
|
+
<Grid2 container spacing={4}>
|
|
228
|
+
{invoice?.CompanyName && (
|
|
229
|
+
<Grid2 size={6}>
|
|
230
|
+
<Typography variant="body2" color="text.secondary">
|
|
231
|
+
From
|
|
232
|
+
</Typography>
|
|
233
|
+
<Typography fontWeight={600}>
|
|
234
|
+
{invoice.CompanyName}
|
|
235
|
+
</Typography>
|
|
236
|
+
{invoice.CompanyEmail && (
|
|
237
|
+
<Typography variant="body2">
|
|
238
|
+
{invoice.CompanyEmail}
|
|
239
|
+
</Typography>
|
|
240
|
+
)}
|
|
241
|
+
{invoice.CompanyAddress && (
|
|
242
|
+
<Typography variant="body2">
|
|
243
|
+
{invoice.CompanyAddress}
|
|
244
|
+
</Typography>
|
|
245
|
+
)}
|
|
246
|
+
</Grid2>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{invoice?.Name && (
|
|
250
|
+
<Grid2 size={6}>
|
|
251
|
+
<Typography variant="body2" color="text.secondary">
|
|
252
|
+
Bill To
|
|
253
|
+
</Typography>
|
|
254
|
+
<Typography fontWeight={600}>{invoice.Name}</Typography>
|
|
255
|
+
{invoice.Email && (
|
|
256
|
+
<Typography variant="body2">{invoice.Email}</Typography>
|
|
257
|
+
)}
|
|
258
|
+
{invoice.BillingAddress && (
|
|
259
|
+
<Typography variant="body2">
|
|
260
|
+
{invoice.BillingAddress}
|
|
261
|
+
</Typography>
|
|
262
|
+
)}
|
|
263
|
+
</Grid2>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{invoice?.InvoiceDate && (
|
|
267
|
+
<Grid2 size={6}>
|
|
268
|
+
<Typography variant="body2" color="text.secondary">
|
|
269
|
+
Invoice Date
|
|
270
|
+
</Typography>
|
|
271
|
+
<Typography>{formatDate(invoice.InvoiceDate)}</Typography>
|
|
272
|
+
</Grid2>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{invoice?.DueDate && (
|
|
276
|
+
<Grid2 size={6}>
|
|
277
|
+
<Typography variant="body2" color="text.secondary">
|
|
278
|
+
Due Date
|
|
279
|
+
</Typography>
|
|
280
|
+
<Typography>{formatDate(invoice.DueDate)}</Typography>
|
|
281
|
+
</Grid2>
|
|
282
|
+
)}
|
|
283
|
+
</Grid2>
|
|
284
|
+
|
|
285
|
+
<Divider sx={{ my: 3 }} />
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Line Items Section */}
|
|
290
|
+
{items.length > 0 ? (
|
|
291
|
+
<>
|
|
292
|
+
{/* Table Header */}
|
|
293
|
+
<Grid2 container sx={{ mb: 1 }}>
|
|
294
|
+
{columns.map((col, i) => (
|
|
295
|
+
<Grid2 key={i} size={col.size}>
|
|
296
|
+
<Typography variant="body2" fontWeight={600}>
|
|
297
|
+
{col.label}
|
|
298
|
+
</Typography>
|
|
299
|
+
</Grid2>
|
|
300
|
+
))}
|
|
301
|
+
</Grid2>
|
|
302
|
+
|
|
303
|
+
{/* Line Items */}
|
|
304
|
+
{items.map((item, idx) => (
|
|
305
|
+
<Grid2
|
|
306
|
+
container
|
|
307
|
+
key={idx}
|
|
308
|
+
sx={{ py: 1.5, borderBottom: "1px solid #eee" }}
|
|
309
|
+
>
|
|
310
|
+
<Grid2 size={1}>
|
|
311
|
+
<Typography variant="body2">{item?.ItemId || "—"}</Typography>
|
|
312
|
+
</Grid2>
|
|
313
|
+
|
|
314
|
+
<Grid2 size={3}>
|
|
315
|
+
<Typography variant="body2">
|
|
316
|
+
{item?.Description || "—"}
|
|
317
|
+
</Typography>
|
|
318
|
+
</Grid2>
|
|
319
|
+
|
|
320
|
+
<Grid2 size={1}>
|
|
321
|
+
<Typography variant="body2">
|
|
322
|
+
{item?.Quantity ?? "—"}
|
|
323
|
+
</Typography>
|
|
324
|
+
</Grid2>
|
|
325
|
+
|
|
326
|
+
<Grid2 size={1.5}>
|
|
327
|
+
<Typography variant="body2">
|
|
328
|
+
{item?.UnitOfMeasure || "—"}
|
|
329
|
+
</Typography>
|
|
330
|
+
</Grid2>
|
|
331
|
+
|
|
332
|
+
<Grid2 size={2}>
|
|
333
|
+
<Typography variant="body2">
|
|
334
|
+
{formatCurrency(item?.Price)}
|
|
335
|
+
</Typography>
|
|
336
|
+
</Grid2>
|
|
337
|
+
|
|
338
|
+
<Grid2 size={1.5}>
|
|
339
|
+
<Typography variant="body2">
|
|
340
|
+
{item?.Savings
|
|
341
|
+
? formatCurrency(item.Savings)
|
|
342
|
+
: formatCurrency(0)}
|
|
343
|
+
</Typography>
|
|
344
|
+
</Grid2>
|
|
345
|
+
|
|
346
|
+
<Grid2 size={1.5}>
|
|
347
|
+
<Typography variant="body2">
|
|
348
|
+
{formatCurrency(item?.LineAmount)}
|
|
349
|
+
</Typography>
|
|
350
|
+
</Grid2>
|
|
351
|
+
|
|
352
|
+
<Grid2
|
|
353
|
+
size={0.5}
|
|
354
|
+
display="flex"
|
|
355
|
+
alignItems="center"
|
|
356
|
+
justifyContent="center"
|
|
357
|
+
>
|
|
358
|
+
{item?.IsOnSale && (
|
|
359
|
+
<Typography color="error.main" fontWeight={700}>
|
|
360
|
+
*
|
|
361
|
+
</Typography>
|
|
362
|
+
)}
|
|
363
|
+
</Grid2>
|
|
364
|
+
</Grid2>
|
|
365
|
+
))}
|
|
366
|
+
|
|
367
|
+
{/* Totals */}
|
|
368
|
+
<Box mt={3} display="flex" justifyContent="flex-end">
|
|
369
|
+
<Box width={300}>
|
|
370
|
+
<Box display="flex" justifyContent="space-between" mb={1}>
|
|
371
|
+
<Typography variant="body2">Subtotal</Typography>
|
|
372
|
+
<Typography variant="body2" fontWeight={500}>
|
|
373
|
+
{formatCurrency(subtotal)}
|
|
374
|
+
</Typography>
|
|
375
|
+
</Box>
|
|
376
|
+
|
|
377
|
+
<Box display="flex" justifyContent="space-between" mb={1}>
|
|
378
|
+
<Typography variant="body2">Tax</Typography>
|
|
379
|
+
<Typography variant="body2" fontWeight={500}>
|
|
380
|
+
{formatCurrency(tax)}
|
|
381
|
+
</Typography>
|
|
382
|
+
</Box>
|
|
383
|
+
|
|
384
|
+
<Divider sx={{ my: 1.5 }} />
|
|
385
|
+
|
|
386
|
+
<Box display="flex" justifyContent="space-between" mt={1}>
|
|
387
|
+
<Typography variant="body1" fontWeight={700}>
|
|
388
|
+
Total
|
|
389
|
+
</Typography>
|
|
390
|
+
<Typography variant="body1" fontWeight={700}>
|
|
391
|
+
{formatCurrency(total)}
|
|
392
|
+
</Typography>
|
|
393
|
+
</Box>
|
|
394
|
+
</Box>
|
|
395
|
+
</Box>
|
|
396
|
+
</>
|
|
397
|
+
) : (
|
|
398
|
+
<Box sx={{ textAlign: "center", py: 4 }}>
|
|
399
|
+
<Typography variant="body2" color="text.secondary">
|
|
400
|
+
No line items to display
|
|
401
|
+
</Typography>
|
|
402
|
+
</Box>
|
|
403
|
+
)}
|
|
404
|
+
</Card>
|
|
405
|
+
</Box>
|
|
406
|
+
);
|
|
407
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StatusColorMap,
|
|
3
|
+
StatusTextMap,
|
|
4
|
+
} from "src/features/table/types/schemas";
|
|
5
|
+
import { InvoiceDetailsTemplate } from "./invoice-details";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared props that every details template must accept.
|
|
9
|
+
* Add optional fields here as the surface area grows; templates
|
|
10
|
+
* that don't need a field can simply ignore it.
|
|
11
|
+
*/
|
|
12
|
+
export interface DetailsTemplateProps {
|
|
13
|
+
data: unknown;
|
|
14
|
+
statusColorMap: StatusColorMap;
|
|
15
|
+
statusTextMap: StatusTextMap;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registry of all available detail templates.
|
|
21
|
+
*
|
|
22
|
+
* To add a new template:
|
|
23
|
+
* 1. Create the component file, implementing DetailsTemplateProps.
|
|
24
|
+
* 2. Import it here.
|
|
25
|
+
* 3. Add a key -> component entry below.
|
|
26
|
+
*
|
|
27
|
+
* The key is what consumers pass as `detailsTemplate` on the resource definition.
|
|
28
|
+
*/
|
|
29
|
+
export const detailsTemplateRegistry: Record<
|
|
30
|
+
string,
|
|
31
|
+
React.ComponentType<DetailsTemplateProps>
|
|
32
|
+
> = {
|
|
33
|
+
invoice: InvoiceDetailsTemplate,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type DetailsTemplateKey = keyof typeof detailsTemplateRegistry;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type * as Table from "../../../types/";
|
|
3
|
+
import { DateFilter } from "./filters/date-filter";
|
|
4
|
+
import { StatusFilter } from "./filters/status-filter";
|
|
5
|
+
|
|
6
|
+
export const filterRenderMapFallback: Table.Handlers.FilterRenderMap = {
|
|
7
|
+
select: (props: Table.Handlers.FilterProps) => <StatusFilter {...props} />,
|
|
8
|
+
date: (props: Table.Handlers.FilterProps) => <DateFilter {...props} />,
|
|
9
|
+
datetime: (props: Table.Handlers.FilterProps) => <DateFilter {...props} />,
|
|
10
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
3
|
+
import Select, { type SelectChangeEvent } from "@mui/material/Select";
|
|
4
|
+
import type * as Table from "../../../../types";
|
|
5
|
+
|
|
6
|
+
export const DateFilter = (props: Table.Handlers.FilterProps) => {
|
|
7
|
+
const { value, filterName, filterOptions, setValue } = props;
|
|
8
|
+
return (
|
|
9
|
+
<Select
|
|
10
|
+
labelId="demo-simple-select-label"
|
|
11
|
+
id="demo-simple-select"
|
|
12
|
+
value={value?.[filterName] || "all"}
|
|
13
|
+
label={filterName}
|
|
14
|
+
sx={{
|
|
15
|
+
minHeight: "100%",
|
|
16
|
+
border: "none",
|
|
17
|
+
backgroundColor: "background.paper",
|
|
18
|
+
}}
|
|
19
|
+
onChange={(e: SelectChangeEvent) => {
|
|
20
|
+
setValue((prev) => ({ ...prev, [filterName]: e.target.value }));
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<MenuItem value={"all"}>All Months</MenuItem>
|
|
24
|
+
{filterOptions?.map((value) => (
|
|
25
|
+
<MenuItem key={value?.value} value={value.value}>
|
|
26
|
+
{value.label}
|
|
27
|
+
</MenuItem>
|
|
28
|
+
))}
|
|
29
|
+
</Select>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
3
|
+
import Select from "@mui/material/Select";
|
|
4
|
+
import type * as Table from "../../../../types/";
|
|
5
|
+
|
|
6
|
+
export const StatusFilter = (props: Table.Handlers.FilterProps) => {
|
|
7
|
+
const { value, filterName, setValue, resetPagination, filterOptions } = props;
|
|
8
|
+
|
|
9
|
+
const selectedValue = value?.[filterName] ?? "all";
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Select
|
|
13
|
+
value={selectedValue}
|
|
14
|
+
sx={{
|
|
15
|
+
minHight: "100%",
|
|
16
|
+
border: "none",
|
|
17
|
+
backgroundColor: "background.paper",
|
|
18
|
+
}}
|
|
19
|
+
onChange={(e) => {
|
|
20
|
+
resetPagination();
|
|
21
|
+
setValue((prev) => ({
|
|
22
|
+
...prev,
|
|
23
|
+
[filterName]: e.target.value,
|
|
24
|
+
}));
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<MenuItem value="all">All Statuses</MenuItem>
|
|
28
|
+
|
|
29
|
+
{filterOptions.map((status) => (
|
|
30
|
+
<MenuItem key={status.label} value={String(status.value)}>
|
|
31
|
+
{status.label}
|
|
32
|
+
</MenuItem>
|
|
33
|
+
))}
|
|
34
|
+
</Select>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import ArticleOutlinedIcon from "@mui/icons-material/ArticleOutlined";
|
|
3
|
+
import { Typography } from "@mui/material";
|
|
4
|
+
import type * as Table from "../../../types/";
|
|
5
|
+
|
|
6
|
+
export const NoResultsComponent: Table.Handlers.NoResultsComponent = ({
|
|
7
|
+
label,
|
|
8
|
+
}: {
|
|
9
|
+
label: string;
|
|
10
|
+
}) => {
|
|
11
|
+
return (
|
|
12
|
+
<Typography
|
|
13
|
+
variant="body2"
|
|
14
|
+
color="text.secondary"
|
|
15
|
+
sx={{
|
|
16
|
+
display: "flex",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
justifyContent: "center",
|
|
19
|
+
gap: 1,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<ArticleOutlinedIcon fontSize="small" />
|
|
23
|
+
No {label.toLowerCase()} found matching your criteria.
|
|
24
|
+
</Typography>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Chip } from "@mui/material";
|
|
2
|
+
import type { StatusColorMap, StatusTextMap } from "../../../types/schemas";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
status: number | string | string[];
|
|
6
|
+
statusTextMap: StatusTextMap;
|
|
7
|
+
statusColorMap: StatusColorMap;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const StatusPill = (props: Props) => {
|
|
11
|
+
const { statusColorMap, statusTextMap, status } = props;
|
|
12
|
+
if (typeof status !== "string" && typeof status !== "number") return null;
|
|
13
|
+
return (
|
|
14
|
+
<Chip label={statusTextMap?.[status]} color={statusColorMap[status]} />
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { StatusPill };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Column } from "@evenicanpm/portal-types-schemas";
|
|
2
|
+
import { Skeleton } from "@mui/material";
|
|
3
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
4
|
+
import { styled } from "@mui/material/styles";
|
|
5
|
+
import TableCell from "@mui/material/TableCell";
|
|
6
|
+
import TableHead from "@mui/material/TableHead";
|
|
7
|
+
import TableRow from "@mui/material/TableRow";
|
|
8
|
+
import type React from "react";
|
|
9
|
+
|
|
10
|
+
// STYLED COMPONENTS
|
|
11
|
+
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
|
12
|
+
fontWeight: 600,
|
|
13
|
+
padding: "16px 20px",
|
|
14
|
+
color: theme.palette.grey[900],
|
|
15
|
+
minWidth: 140,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
interface TableHeaderProps {
|
|
19
|
+
hasActions: boolean;
|
|
20
|
+
columns: (Column & { ColumnOrder: number })[] | undefined;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
selectAllChecked: boolean;
|
|
23
|
+
selectAllIndeterminate: boolean;
|
|
24
|
+
onToggleAll: (checked: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
*
|
|
29
|
+
* Table handles display and caches it's own data.
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
export default function TableHeader({
|
|
33
|
+
hasActions,
|
|
34
|
+
columns,
|
|
35
|
+
loading,
|
|
36
|
+
selectAllChecked,
|
|
37
|
+
selectAllIndeterminate,
|
|
38
|
+
onToggleAll,
|
|
39
|
+
}: TableHeaderProps) {
|
|
40
|
+
if (loading) {
|
|
41
|
+
return (
|
|
42
|
+
<TableHead>
|
|
43
|
+
<TableRow>
|
|
44
|
+
<StyledTableCell padding="checkbox" sx={{ width: 56 }}>
|
|
45
|
+
<Skeleton
|
|
46
|
+
variant="rounded"
|
|
47
|
+
width={24}
|
|
48
|
+
height={24}
|
|
49
|
+
sx={{ bgcolor: "rgba(255,255,255,0.1)" }}
|
|
50
|
+
/>
|
|
51
|
+
</StyledTableCell>
|
|
52
|
+
{Array.from({ length: 6 })?.map((_col, i) => (
|
|
53
|
+
<StyledTableCell key={String(i)}>
|
|
54
|
+
<Skeleton
|
|
55
|
+
variant="rounded"
|
|
56
|
+
width={100}
|
|
57
|
+
height={25}
|
|
58
|
+
sx={{ bgcolor: "rgba(255,255,255,0.1)" }}
|
|
59
|
+
/>
|
|
60
|
+
</StyledTableCell>
|
|
61
|
+
))}
|
|
62
|
+
{hasActions && (
|
|
63
|
+
<StyledTableCell>
|
|
64
|
+
<Skeleton variant="rounded" width={100} height={25} />
|
|
65
|
+
</StyledTableCell>
|
|
66
|
+
)}
|
|
67
|
+
</TableRow>
|
|
68
|
+
</TableHead>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<TableHead>
|
|
73
|
+
<TableRow>
|
|
74
|
+
<StyledTableCell sx={{ padding: "16px 16px", width: 72 }}>
|
|
75
|
+
<Checkbox
|
|
76
|
+
checked={selectAllChecked}
|
|
77
|
+
indeterminate={selectAllIndeterminate}
|
|
78
|
+
onChange={(_e, checked) => onToggleAll(checked)}
|
|
79
|
+
inputProps={{ "aria-label": "select all rows" }}
|
|
80
|
+
/>
|
|
81
|
+
</StyledTableCell>
|
|
82
|
+
|
|
83
|
+
{columns?.map((column, i) => (
|
|
84
|
+
<StyledTableCell
|
|
85
|
+
key={column.Label + String(i)}
|
|
86
|
+
sx={{
|
|
87
|
+
whiteSpace: "normal",
|
|
88
|
+
wordBreak: "break-word",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{column.Label}
|
|
92
|
+
</StyledTableCell>
|
|
93
|
+
))}
|
|
94
|
+
{hasActions && <StyledTableCell>Actions</StyledTableCell>}
|
|
95
|
+
</TableRow>
|
|
96
|
+
</TableHead>
|
|
97
|
+
);
|
|
98
|
+
}
|