@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evenicanpm/portal-table-ui",
3
- "version": "1.7.0",
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.7.0",
45
- "@evenicanpm/ui": "^1.6.0",
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": "2e25d32705901a4ff2dd43aa420fde931568ad93"
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 { Row } from "src/features/table/types/queries";
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
- LineAmount?: number;
42
+ NetAmount?: number;
49
43
  UnitOfMeasure?: string;
50
- FullDescription?: string;
51
- Savings?: number;
52
- IsOnSale?: boolean;
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: 2 },
113
+ { label: "Unit", size: 1 },
116
114
  { label: "Unit Price", size: 1.5 },
117
- { label: "Savings", size: 1.5 },
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
- <Grid2 size={6} key={i}>
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, i) => (
142
- <Grid2 key={i} size={col.size}>
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
- <Grid2 container key={idx} sx={{ py: 1.5 }}>
149
- {columns.map((col, i) => (
150
- <Grid2 key={i} size={col.size}>
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={i}
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
- {invoice?.Status !== 3 && invoice?.Status !== undefined && (
193
- <Button variant="contained" color="warning">
194
- Pay Now
195
- </Button>
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, i) => (
295
- <Grid2 key={i} size={col.size}>
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
- <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>
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
- <Grid2 size={1}>
321
- <Typography variant="body2">
322
- {item?.Quantity ?? "—"}
323
- </Typography>
324
- </Grid2>
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
- <Grid2 size={1.5}>
327
- <Typography variant="body2">
328
- {item?.UnitOfMeasure || "—"}
329
- </Typography>
330
- </Grid2>
347
+ <Grid2 size={3.5}>
348
+ <Typography variant="body2">{productName}</Typography>
349
+ </Grid2>
331
350
 
332
- <Grid2 size={2}>
333
- <Typography variant="body2">
334
- {formatCurrency(item?.Price)}
335
- </Typography>
336
- </Grid2>
351
+ <Grid2 size={1}>
352
+ <Typography variant="body2">{quantity}</Typography>
353
+ </Grid2>
337
354
 
338
- <Grid2 size={1.5}>
339
- <Typography variant="body2">
340
- {item?.Savings
341
- ? formatCurrency(item.Savings)
342
- : formatCurrency(0)}
343
- </Typography>
344
- </Grid2>
355
+ <Grid2 size={1}>
356
+ <Typography variant="body2">
357
+ {item?.UnitOfMeasure || "—"}
358
+ </Typography>
359
+ </Grid2>
345
360
 
346
- <Grid2 size={1.5}>
347
- <Typography variant="body2">
348
- {formatCurrency(item?.LineAmount)}
349
- </Typography>
350
- </Grid2>
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
- <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
- *
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
- </Grid2>
365
- ))}
397
+ );
398
+ })}
366
399
 
367
400
  {/* Totals */}
368
401
  <Box mt={3} display="flex" justifyContent="flex-end">
@@ -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
  /**
@@ -1,4 +1,3 @@
1
- import { Action } from "@evenicanpm/portal-types-schemas";
2
1
  import { FlexBox } from "@evenicanpm/ui";
3
2
  import Checkbox from "@mui/material/Checkbox";
4
3
  import TableCell from "@mui/material/TableCell";
@@ -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;