@evenicanpm/portal-table-ui 1.7.0 → 1.8.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 +4 -4
- package/src/features/table/components/dashboard/table/bulk-action-button.tsx +3 -4
- package/src/features/table/components/dashboard/table/details/details.tsx +7 -0
- package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +118 -85
- package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +3 -0
- package/src/features/table/components/dashboard/table/table-row/table-row.tsx +0 -1
- package/src/features/table/components/dashboard/table/table.tsx +20 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evenicanpm/portal-table-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@arrows/multimethod": "^2.1.0",
|
|
44
|
-
"@evenicanpm/portal-types-schemas": "^1.
|
|
45
|
-
"@evenicanpm/ui": "^1.
|
|
44
|
+
"@evenicanpm/portal-types-schemas": "^1.8.0",
|
|
45
|
+
"@evenicanpm/ui": "^1.8.0",
|
|
46
46
|
"@mui/lab": "^7.0.1-beta.19",
|
|
47
47
|
"@tanstack/react-query": "^5.90.11",
|
|
48
48
|
"date-fns": "^4.1.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "485f77c5cce7c36849fb9d416d1e503d0f9299a3"
|
|
51
51
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Badge, Button, CircularProgress, IconButton } from "@mui/material";
|
|
2
2
|
import { useMutation } from "@tanstack/react-query";
|
|
3
|
-
import type
|
|
4
|
-
import type { UIBulkAction } from "src/features/table/types/schemas";
|
|
3
|
+
import type * as Types from "../../../types";
|
|
5
4
|
|
|
6
5
|
interface BulkActionButtonProps {
|
|
7
|
-
action: UIBulkAction;
|
|
6
|
+
action: Types.Schemas.UIBulkAction;
|
|
8
7
|
resourceName: string;
|
|
9
8
|
selectedIds: (string | number)[];
|
|
10
9
|
clearSelection: () => void;
|
|
11
|
-
selectedRows: Row[];
|
|
10
|
+
selectedRows: Types.Queries.Row[];
|
|
12
11
|
}
|
|
13
12
|
export function BulkActionButton({
|
|
14
13
|
action,
|
|
@@ -34,6 +34,9 @@ interface DetailsProps {
|
|
|
34
34
|
statusTextMap: StatusTextMap;
|
|
35
35
|
template?: string;
|
|
36
36
|
loading?: boolean;
|
|
37
|
+
|
|
38
|
+
actions?: Types.Schemas.UIAction[];
|
|
39
|
+
resourceName?: string;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -49,6 +52,8 @@ const Details = ({
|
|
|
49
52
|
statusColorMap,
|
|
50
53
|
statusTextMap,
|
|
51
54
|
template,
|
|
55
|
+
actions,
|
|
56
|
+
resourceName,
|
|
52
57
|
loading = false,
|
|
53
58
|
}: DetailsProps) => {
|
|
54
59
|
// Resolve the template component once — undefined when the key is absent or
|
|
@@ -96,6 +101,8 @@ const Details = ({
|
|
|
96
101
|
statusColorMap={statusColorMap}
|
|
97
102
|
statusTextMap={statusTextMap}
|
|
98
103
|
loading={loading}
|
|
104
|
+
actions={actions}
|
|
105
|
+
resourceName={resourceName}
|
|
99
106
|
/>
|
|
100
107
|
) : (
|
|
101
108
|
<Grid container spacing={2}>
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
Box,
|
|
4
|
-
Button,
|
|
5
|
-
Card,
|
|
6
|
-
Divider,
|
|
7
|
-
Skeleton,
|
|
8
|
-
Typography,
|
|
9
|
-
} from "@mui/material";
|
|
2
|
+
import { Box, Card, Divider, Skeleton, Typography } from "@mui/material";
|
|
10
3
|
import Grid2 from "@mui/material/Grid2";
|
|
11
4
|
import { StatusPill } from "../../status-pill";
|
|
5
|
+
import { ActionButton } from "../../table-row/action-button";
|
|
12
6
|
import type { DetailsTemplateProps } from "./template-registry";
|
|
13
7
|
|
|
14
8
|
// Formatting utilities
|
|
@@ -41,15 +35,15 @@ const formatDate = (dateString?: string): string => {
|
|
|
41
35
|
};
|
|
42
36
|
|
|
43
37
|
interface InvoiceLine {
|
|
44
|
-
Description?: string;
|
|
45
38
|
ItemId?: string;
|
|
39
|
+
ProductName?: string;
|
|
46
40
|
Quantity?: number;
|
|
47
41
|
Price?: number;
|
|
48
|
-
|
|
42
|
+
NetAmount?: number;
|
|
49
43
|
UnitOfMeasure?: string;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
LineDiscount?: number;
|
|
45
|
+
DiscountAmount: number;
|
|
46
|
+
HasDiscount: boolean;
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
interface InvoiceData {
|
|
@@ -67,6 +61,7 @@ interface InvoiceData {
|
|
|
67
61
|
InvoiceDate?: string;
|
|
68
62
|
DueDate?: string;
|
|
69
63
|
Notes?: string;
|
|
64
|
+
Id: string;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
const isDataEmpty = (data: InvoiceData | null | undefined): boolean => {
|
|
@@ -85,6 +80,8 @@ export const InvoiceDetailsTemplate = ({
|
|
|
85
80
|
statusColorMap,
|
|
86
81
|
statusTextMap,
|
|
87
82
|
loading = false,
|
|
83
|
+
actions,
|
|
84
|
+
resourceName = "invoice",
|
|
88
85
|
}: DetailsTemplateProps) => {
|
|
89
86
|
const invoice = data as InvoiceData;
|
|
90
87
|
|
|
@@ -108,13 +105,14 @@ export const InvoiceDetailsTemplate = ({
|
|
|
108
105
|
const tax = invoice?.TotalTaxAmount ?? 0;
|
|
109
106
|
const total = invoice?.Amount ?? 0;
|
|
110
107
|
|
|
108
|
+
// Fixed column widths to sum to 12
|
|
111
109
|
const columns = [
|
|
112
|
-
{ label: "#", size: 1 },
|
|
113
|
-
{ label: "Product", size: 3 },
|
|
110
|
+
{ label: "#", size: 1.5 },
|
|
111
|
+
{ label: "Product", size: 3.5 },
|
|
114
112
|
{ label: "Qty", size: 1 },
|
|
115
|
-
{ label: "Unit", size:
|
|
113
|
+
{ label: "Unit", size: 1 },
|
|
116
114
|
{ label: "Unit Price", size: 1.5 },
|
|
117
|
-
{ label: "
|
|
115
|
+
{ label: "Discount", size: 1.5 },
|
|
118
116
|
{ label: "Line Total", size: 1.5 },
|
|
119
117
|
{ label: "", size: 0.5 },
|
|
120
118
|
];
|
|
@@ -129,7 +127,8 @@ export const InvoiceDetailsTemplate = ({
|
|
|
129
127
|
<Card sx={{ p: 4, borderRadius: 3 }}>
|
|
130
128
|
<Grid2 container spacing={4}>
|
|
131
129
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
132
|
-
|
|
130
|
+
// biome-ignore lint: no-array-index-key
|
|
131
|
+
<Grid2 size={6} key={`skeleton-${i}`}>
|
|
133
132
|
<Skeleton width="40%" height={20} />
|
|
134
133
|
<Skeleton width="80%" />
|
|
135
134
|
<Skeleton width="60%" />
|
|
@@ -138,16 +137,17 @@ export const InvoiceDetailsTemplate = ({
|
|
|
138
137
|
</Grid2>
|
|
139
138
|
<Divider sx={{ my: 3 }} />
|
|
140
139
|
<Grid2 container sx={{ mb: 2 }}>
|
|
141
|
-
{columns.map((col
|
|
142
|
-
<Grid2 key={
|
|
140
|
+
{columns.map((col) => (
|
|
141
|
+
<Grid2 key={col.label} size={col.size}>
|
|
143
142
|
<Skeleton width="60%" />
|
|
144
143
|
</Grid2>
|
|
145
144
|
))}
|
|
146
145
|
</Grid2>
|
|
147
146
|
{Array.from({ length: 4 }).map((_, idx) => (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// biome-ignore lint: no-array-index-key
|
|
148
|
+
<Grid2 container key={`skeleton-${idx}`} sx={{ py: 1.5 }}>
|
|
149
|
+
{columns.map((col) => (
|
|
150
|
+
<Grid2 key={col.label} size={col.size}>
|
|
151
151
|
<Skeleton width="80%" />
|
|
152
152
|
</Grid2>
|
|
153
153
|
))}
|
|
@@ -157,7 +157,8 @@ export const InvoiceDetailsTemplate = ({
|
|
|
157
157
|
<Box width={300}>
|
|
158
158
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
159
159
|
<Box
|
|
160
|
-
key
|
|
160
|
+
// biome-ignore lint: no-array-index-key
|
|
161
|
+
key={`skeleton-${i}`}
|
|
161
162
|
display="flex"
|
|
162
163
|
justifyContent="space-between"
|
|
163
164
|
mb={1}
|
|
@@ -189,11 +190,35 @@ export const InvoiceDetailsTemplate = ({
|
|
|
189
190
|
statusTextMap={statusTextMap}
|
|
190
191
|
/>
|
|
191
192
|
)}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
<Box display="flex" gap={2}>
|
|
194
|
+
{actions?.map((action) => {
|
|
195
|
+
const isPaid = invoice.Status === 3; // Need to pass table.row to action to see if enabled
|
|
196
|
+
|
|
197
|
+
const enabled = !isPaid && action.isEnabled;
|
|
198
|
+
|
|
199
|
+
if (!enabled) {
|
|
200
|
+
return (
|
|
201
|
+
<span
|
|
202
|
+
key={action.label}
|
|
203
|
+
style={{
|
|
204
|
+
display: "inline-block",
|
|
205
|
+
width: action.icon ? "34px" : "42px",
|
|
206
|
+
height: "34px",
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<ActionButton
|
|
214
|
+
key={action.label}
|
|
215
|
+
docId={invoice.Id}
|
|
216
|
+
action={action}
|
|
217
|
+
resourceName={resourceName}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</Box>
|
|
197
222
|
</Box>
|
|
198
223
|
|
|
199
224
|
{/* Notes Section */}
|
|
@@ -291,8 +316,8 @@ export const InvoiceDetailsTemplate = ({
|
|
|
291
316
|
<>
|
|
292
317
|
{/* Table Header */}
|
|
293
318
|
<Grid2 container sx={{ mb: 1 }}>
|
|
294
|
-
{columns.map((col
|
|
295
|
-
<Grid2 key={
|
|
319
|
+
{columns.map((col) => (
|
|
320
|
+
<Grid2 key={col.label} size={col.size}>
|
|
296
321
|
<Typography variant="body2" fontWeight={600}>
|
|
297
322
|
{col.label}
|
|
298
323
|
</Typography>
|
|
@@ -301,68 +326,76 @@ export const InvoiceDetailsTemplate = ({
|
|
|
301
326
|
</Grid2>
|
|
302
327
|
|
|
303
328
|
{/* Line Items */}
|
|
304
|
-
{items.map((item, idx) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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>
|
|
329
|
+
{items.map((item, idx) => {
|
|
330
|
+
const productName = item.ProductName || "-";
|
|
331
|
+
const unitPrice = item.Price ?? 0;
|
|
332
|
+
const quantity = item.Quantity ?? 0;
|
|
333
|
+
const lineTotal = item.NetAmount ?? unitPrice * quantity;
|
|
319
334
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
return (
|
|
336
|
+
<Grid2
|
|
337
|
+
container
|
|
338
|
+
key={`${item.ItemId}-${idx}`}
|
|
339
|
+
sx={{ py: 1.5, borderBottom: "1px solid #eee" }}
|
|
340
|
+
>
|
|
341
|
+
<Grid2 size={1.5}>
|
|
342
|
+
<Typography variant="body2">
|
|
343
|
+
{item?.ItemId || idx + 1}
|
|
344
|
+
</Typography>
|
|
345
|
+
</Grid2>
|
|
325
346
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
</Typography>
|
|
330
|
-
</Grid2>
|
|
347
|
+
<Grid2 size={3.5}>
|
|
348
|
+
<Typography variant="body2">{productName}</Typography>
|
|
349
|
+
</Grid2>
|
|
331
350
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
</Typography>
|
|
336
|
-
</Grid2>
|
|
351
|
+
<Grid2 size={1}>
|
|
352
|
+
<Typography variant="body2">{quantity}</Typography>
|
|
353
|
+
</Grid2>
|
|
337
354
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
</Typography>
|
|
344
|
-
</Grid2>
|
|
355
|
+
<Grid2 size={1}>
|
|
356
|
+
<Typography variant="body2">
|
|
357
|
+
{item?.UnitOfMeasure || "—"}
|
|
358
|
+
</Typography>
|
|
359
|
+
</Grid2>
|
|
345
360
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
<Grid2 size={1.5}>
|
|
362
|
+
<Typography variant="body2">
|
|
363
|
+
{formatCurrency(unitPrice)}
|
|
364
|
+
</Typography>
|
|
365
|
+
</Grid2>
|
|
366
|
+
|
|
367
|
+
<Grid2 size={1.5}>
|
|
368
|
+
<Typography
|
|
369
|
+
variant="body2"
|
|
370
|
+
color={item.HasDiscount ? "error.main" : "text.secondary"}
|
|
371
|
+
>
|
|
372
|
+
{item.HasDiscount
|
|
373
|
+
? `-${formatCurrency(item.DiscountAmount)}`
|
|
374
|
+
: formatCurrency(0)}
|
|
375
|
+
</Typography>
|
|
376
|
+
</Grid2>
|
|
351
377
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
alignItems="center"
|
|
356
|
-
justifyContent="center"
|
|
357
|
-
>
|
|
358
|
-
{item?.IsOnSale && (
|
|
359
|
-
<Typography color="error.main" fontWeight={700}>
|
|
360
|
-
*
|
|
378
|
+
<Grid2 size={1.5}>
|
|
379
|
+
<Typography variant="body2" fontWeight={500}>
|
|
380
|
+
{formatCurrency(lineTotal)}
|
|
361
381
|
</Typography>
|
|
362
|
-
|
|
382
|
+
</Grid2>
|
|
383
|
+
|
|
384
|
+
<Grid2
|
|
385
|
+
size={0.5}
|
|
386
|
+
display="flex"
|
|
387
|
+
alignItems="center"
|
|
388
|
+
justifyContent="center"
|
|
389
|
+
>
|
|
390
|
+
{item.HasDiscount && (
|
|
391
|
+
<Typography color="error.main" fontWeight={700}>
|
|
392
|
+
*
|
|
393
|
+
</Typography>
|
|
394
|
+
)}
|
|
395
|
+
</Grid2>
|
|
363
396
|
</Grid2>
|
|
364
|
-
|
|
365
|
-
)
|
|
397
|
+
);
|
|
398
|
+
})}
|
|
366
399
|
|
|
367
400
|
{/* Totals */}
|
|
368
401
|
<Box mt={3} display="flex" justifyContent="flex-end">
|
package/src/features/table/components/dashboard/table/details/templates/template-registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Table } from "src/features/table";
|
|
1
2
|
import type {
|
|
2
3
|
StatusColorMap,
|
|
3
4
|
StatusTextMap,
|
|
@@ -14,6 +15,8 @@ export interface DetailsTemplateProps {
|
|
|
14
15
|
statusColorMap: StatusColorMap;
|
|
15
16
|
statusTextMap: StatusTextMap;
|
|
16
17
|
loading?: boolean;
|
|
18
|
+
actions?: Table.Schemas.UIAction[];
|
|
19
|
+
resourceName?: string;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/**
|
|
@@ -203,6 +203,24 @@ export default function DocumentTable({
|
|
|
203
203
|
},
|
|
204
204
|
);
|
|
205
205
|
|
|
206
|
+
const resolvedActions = resolvers.resolveActions(
|
|
207
|
+
config?.Actions,
|
|
208
|
+
actionRegistry,
|
|
209
|
+
);
|
|
210
|
+
const resolvedBulkActions = resolvers.resolveBulkActions(
|
|
211
|
+
config?.BulkActions,
|
|
212
|
+
bulkRegistry,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const hasActions =
|
|
216
|
+
configPending ||
|
|
217
|
+
(resolvedActions.length > 0 &&
|
|
218
|
+
rows?.data?.some((row) =>
|
|
219
|
+
resolvedActions.some((a) => !a.isEnabled || a.isEnabled(row)),
|
|
220
|
+
)) ||
|
|
221
|
+
false;
|
|
222
|
+
const hasBulkActions = resolvedBulkActions.length > 0;
|
|
223
|
+
|
|
206
224
|
// --- Render --- //
|
|
207
225
|
if (selectedDocument) {
|
|
208
226
|
const rowSnapshot =
|
|
@@ -224,28 +242,12 @@ export default function DocumentTable({
|
|
|
224
242
|
detailsRenderMap={detailsRenderMap}
|
|
225
243
|
template={hasTemplateSupport ? resource.detailsTemplate : undefined}
|
|
226
244
|
loading={hasTemplateSupport ? detailsPending : false}
|
|
245
|
+
actions={resolvedActions}
|
|
246
|
+
resourceName={resource.name}
|
|
227
247
|
/>
|
|
228
248
|
);
|
|
229
249
|
}
|
|
230
250
|
|
|
231
|
-
const resolvedActions = resolvers.resolveActions(
|
|
232
|
-
config?.Actions,
|
|
233
|
-
actionRegistry,
|
|
234
|
-
);
|
|
235
|
-
const resolvedBulkActions = resolvers.resolveBulkActions(
|
|
236
|
-
config?.BulkActions,
|
|
237
|
-
bulkRegistry,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
const hasActions =
|
|
241
|
-
configPending ||
|
|
242
|
-
(resolvedActions.length > 0 &&
|
|
243
|
-
rows?.data?.some((row) =>
|
|
244
|
-
resolvedActions.some((a) => !a.isEnabled || a.isEnabled(row)),
|
|
245
|
-
)) ||
|
|
246
|
-
false;
|
|
247
|
-
const hasBulkActions = resolvedBulkActions.length > 0;
|
|
248
|
-
|
|
249
251
|
const baseCols = (visibleCols?.length ?? 0) + 1;
|
|
250
252
|
const colSpan = hasActions ? baseCols + 1 : baseCols;
|
|
251
253
|
const noResults = !rowsPending && !configPending && rows?.data?.length === 0;
|