@evenicanpm/portal-table-ui 1.8.0 → 1.8.1

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.8.0",
3
+ "version": "1.8.1",
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.8.0",
45
- "@evenicanpm/ui": "^1.8.0",
44
+ "@evenicanpm/portal-types-schemas": "^1.8.1",
45
+ "@evenicanpm/ui": "^1.8.1",
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": "485f77c5cce7c36849fb9d416d1e503d0f9299a3"
50
+ "gitHead": "07cfd4496c6a137e1420a6572298998b25b6369d"
51
51
  }
@@ -1,5 +1,20 @@
1
1
  "use client";
2
- import { Box, Card, Divider, Skeleton, Typography } from "@mui/material";
2
+ import {
3
+ Box,
4
+ Card,
5
+ Divider,
6
+ Paper,
7
+ Skeleton,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableContainer,
12
+ TableHead,
13
+ TableRow,
14
+ Typography,
15
+ useMediaQuery,
16
+ useTheme,
17
+ } from "@mui/material";
3
18
  import Grid2 from "@mui/material/Grid2";
4
19
  import { StatusPill } from "../../status-pill";
5
20
  import { ActionButton } from "../../table-row/action-button";
@@ -18,12 +33,9 @@ const formatCurrency = (amount?: number): string => {
18
33
 
19
34
  const formatDate = (dateString?: string): string => {
20
35
  if (!dateString) return "—";
21
-
22
36
  try {
23
37
  const date = new Date(dateString);
24
- // Check if date is valid
25
38
  if (isNaN(date.getTime())) return dateString;
26
-
27
39
  return new Intl.DateTimeFormat("en-US", {
28
40
  year: "numeric",
29
41
  month: "long",
@@ -66,12 +78,10 @@ interface InvoiceData {
66
78
 
67
79
  const isDataEmpty = (data: InvoiceData | null | undefined): boolean => {
68
80
  if (!data) return true;
69
-
70
81
  const hasNoLineItems =
71
82
  !data.SalesInvoiceLine || data.SalesInvoiceLine.length === 0;
72
83
  const hasNoBasicInfo = !data.CompanyName && !data.Name && !data.InvoiceDate;
73
84
  const hasNoAmounts = !data.Amount && data.Amount !== 0;
74
-
75
85
  return hasNoLineItems && hasNoBasicInfo && hasNoAmounts;
76
86
  };
77
87
 
@@ -83,17 +93,25 @@ export const InvoiceDetailsTemplate = ({
83
93
  actions,
84
94
  resourceName = "invoice",
85
95
  }: DetailsTemplateProps) => {
96
+ const theme = useTheme();
97
+ const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
98
+
86
99
  const invoice = data as InvoiceData;
87
100
 
101
+ // Responsive width
102
+ const containerWidth = isMobile ? "95%" : "75%";
103
+
88
104
  if (!loading && (!invoice || isDataEmpty(invoice))) {
89
105
  return (
90
- <Box sx={{ p: 3, width: "75%", mx: "auto" }}>
91
- <Card sx={{ p: 6, borderRadius: 3, textAlign: "center" }}>
106
+ <Box sx={{ p: { xs: 2, sm: 3 }, width: containerWidth, mx: "auto" }}>
107
+ <Card
108
+ sx={{ p: { xs: 3, sm: 6 }, borderRadius: 3, textAlign: "center" }}
109
+ >
92
110
  <Typography variant="h6" color="text.secondary" gutterBottom>
93
111
  No Data Available
94
112
  </Typography>
95
113
  <Typography variant="body2" color="text.secondary">
96
- This invoice template has no data to display.
114
+ We couldn't find any details for selected invoice.
97
115
  </Typography>
98
116
  </Card>
99
117
  </Box>
@@ -105,59 +123,79 @@ export const InvoiceDetailsTemplate = ({
105
123
  const tax = invoice?.TotalTaxAmount ?? 0;
106
124
  const total = invoice?.Amount ?? 0;
107
125
 
108
- // Fixed column widths to sum to 12
126
+ // Fixed column definitions with specific widths
109
127
  const columns = [
110
- { label: "#", size: 1.5 },
111
- { label: "Product", size: 3.5 },
112
- { label: "Qty", size: 1 },
113
- { label: "Unit", size: 1 },
114
- { label: "Unit Price", size: 1.5 },
115
- { label: "Discount", size: 1.5 },
116
- { label: "Line Total", size: 1.5 },
117
- { label: "", size: 0.5 },
128
+ { label: "#", width: 60 },
129
+ { label: "Product", width: 200 },
130
+ { label: "Qty", width: 60 },
131
+ { label: "Unit", width: 60 },
132
+ { label: "Unit Price", width: 120 },
133
+ { label: "Discount", width: 100 },
134
+ { label: "Line Total", width: 120 },
135
+ { label: "", width: 40 },
118
136
  ];
119
137
 
138
+ // Calculate total width for table container
139
+ const totalTableWidth = columns.reduce((sum, col) => sum + col.width, 0);
140
+
120
141
  if (loading) {
121
142
  return (
122
- <Box sx={{ p: 3, width: "75%", mx: "auto" }}>
143
+ <Box sx={{ p: { xs: 2, sm: 3 }, width: containerWidth, mx: "auto" }}>
123
144
  <Box display="flex" justifyContent="space-between" mb={3}>
124
145
  <Skeleton variant="rectangular" width={100} height={32} />
125
146
  <Skeleton variant="rectangular" width={120} height={40} />
126
147
  </Box>
127
- <Card sx={{ p: 4, borderRadius: 3 }}>
128
- <Grid2 container spacing={4}>
148
+ <Card sx={{ p: { xs: 2, sm: 4 }, borderRadius: 3 }}>
149
+ <Grid2 container spacing={{ xs: 2, sm: 4 }}>
129
150
  {Array.from({ length: 4 }).map((_, i) => (
130
- // biome-ignore lint: no-array-index-key
131
- <Grid2 size={6} key={`skeleton-${i}`}>
151
+ <Grid2 size={{ xs: 12, sm: 6 }} key={`skeleton-${i}`}>
132
152
  <Skeleton width="40%" height={20} />
133
153
  <Skeleton width="80%" />
134
154
  <Skeleton width="60%" />
135
155
  </Grid2>
136
156
  ))}
137
157
  </Grid2>
138
- <Divider sx={{ my: 3 }} />
139
- <Grid2 container sx={{ mb: 2 }}>
140
- {columns.map((col) => (
141
- <Grid2 key={col.label} size={col.size}>
142
- <Skeleton width="60%" />
143
- </Grid2>
144
- ))}
145
- </Grid2>
146
- {Array.from({ length: 4 }).map((_, idx) => (
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
- <Skeleton width="80%" />
152
- </Grid2>
153
- ))}
154
- </Grid2>
155
- ))}
158
+ <Divider sx={{ my: { xs: 2, sm: 3 } }} />
159
+
160
+ {/* Table skeleton with scroll */}
161
+ <TableContainer
162
+ component={Paper}
163
+ sx={{ boxShadow: "none", overflowX: "auto" }}
164
+ >
165
+ <Table sx={{ minWidth: totalTableWidth }}>
166
+ <TableHead>
167
+ <TableRow>
168
+ {columns.map((col) => (
169
+ <TableCell
170
+ key={col.label}
171
+ sx={{ width: col.width, minWidth: col.width }}
172
+ >
173
+ <Skeleton width="80%" />
174
+ </TableCell>
175
+ ))}
176
+ </TableRow>
177
+ </TableHead>
178
+ <TableBody>
179
+ {Array.from({ length: 4 }).map((_, idx) => (
180
+ <TableRow key={`skeleton-${idx}`}>
181
+ {columns.map((col) => (
182
+ <TableCell
183
+ key={col.label}
184
+ sx={{ width: col.width, minWidth: col.width }}
185
+ >
186
+ <Skeleton width="80%" />
187
+ </TableCell>
188
+ ))}
189
+ </TableRow>
190
+ ))}
191
+ </TableBody>
192
+ </Table>
193
+ </TableContainer>
194
+
156
195
  <Box mt={3} display="flex" justifyContent="flex-end">
157
- <Box width={300}>
196
+ <Box width={{ xs: "100%", sm: 300 }}>
158
197
  {Array.from({ length: 3 }).map((_, i) => (
159
198
  <Box
160
- // biome-ignore lint: no-array-index-key
161
199
  key={`skeleton-${i}`}
162
200
  display="flex"
163
201
  justifyContent="space-between"
@@ -175,12 +213,14 @@ export const InvoiceDetailsTemplate = ({
175
213
  }
176
214
 
177
215
  return (
178
- <Box sx={{ p: 3, width: "75%", mx: "auto" }}>
216
+ <Box sx={{ p: { xs: 2, sm: 3 }, width: containerWidth, mx: "auto" }}>
179
217
  {/* Header */}
180
218
  <Box
181
219
  display="flex"
182
220
  justifyContent="space-between"
183
221
  alignItems="center"
222
+ flexWrap="wrap"
223
+ gap={2}
184
224
  mb={3}
185
225
  >
186
226
  {invoice?.Status !== undefined && (
@@ -190,12 +230,10 @@ export const InvoiceDetailsTemplate = ({
190
230
  statusTextMap={statusTextMap}
191
231
  />
192
232
  )}
193
- <Box display="flex" gap={2}>
233
+ <Box display="flex" gap={2} alignItems="center">
194
234
  {actions?.map((action) => {
195
- const isPaid = invoice.Status === 3; // Need to pass table.row to action to see if enabled
196
-
235
+ const isPaid = invoice.Status === 3;
197
236
  const enabled = !isPaid && action.isEnabled;
198
-
199
237
  if (!enabled) {
200
238
  return (
201
239
  <span
@@ -208,7 +246,6 @@ export const InvoiceDetailsTemplate = ({
208
246
  />
209
247
  );
210
248
  }
211
-
212
249
  return (
213
250
  <ActionButton
214
251
  key={action.label}
@@ -223,7 +260,7 @@ export const InvoiceDetailsTemplate = ({
223
260
 
224
261
  {/* Notes Section */}
225
262
  {invoice?.Notes && (
226
- <Card sx={{ p: 3, borderRadius: 3, mb: 3 }}>
263
+ <Card sx={{ p: { xs: 2, sm: 3 }, borderRadius: 3, mb: 3 }}>
227
264
  <Typography variant="subtitle1" color="text.secondary" mb={1}>
228
265
  Notes
229
266
  </Typography>
@@ -238,7 +275,7 @@ export const InvoiceDetailsTemplate = ({
238
275
  )}
239
276
 
240
277
  {/* Main Invoice Card */}
241
- <Card sx={{ p: 3, borderRadius: 3 }}>
278
+ <Card sx={{ p: { xs: 2, sm: 3 }, borderRadius: 3 }}>
242
279
  {/* Details Section */}
243
280
  {(invoice?.CompanyName ||
244
281
  invoice?.Name ||
@@ -248,10 +285,9 @@ export const InvoiceDetailsTemplate = ({
248
285
  <Typography variant="subtitle1" color="text.secondary" mb={2}>
249
286
  Details
250
287
  </Typography>
251
-
252
- <Grid2 container spacing={4}>
288
+ <Grid2 container spacing={{ xs: 2, sm: 4 }}>
253
289
  {invoice?.CompanyName && (
254
- <Grid2 size={6}>
290
+ <Grid2 size={{ xs: 12, sm: 6 }}>
255
291
  <Typography variant="body2" color="text.secondary">
256
292
  From
257
293
  </Typography>
@@ -259,46 +295,45 @@ export const InvoiceDetailsTemplate = ({
259
295
  {invoice.CompanyName}
260
296
  </Typography>
261
297
  {invoice.CompanyEmail && (
262
- <Typography variant="body2">
298
+ <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
263
299
  {invoice.CompanyEmail}
264
300
  </Typography>
265
301
  )}
266
302
  {invoice.CompanyAddress && (
267
- <Typography variant="body2">
303
+ <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
268
304
  {invoice.CompanyAddress}
269
305
  </Typography>
270
306
  )}
271
307
  </Grid2>
272
308
  )}
273
-
274
309
  {invoice?.Name && (
275
- <Grid2 size={6}>
310
+ <Grid2 size={{ xs: 12, sm: 6 }}>
276
311
  <Typography variant="body2" color="text.secondary">
277
312
  Bill To
278
313
  </Typography>
279
314
  <Typography fontWeight={600}>{invoice.Name}</Typography>
280
315
  {invoice.Email && (
281
- <Typography variant="body2">{invoice.Email}</Typography>
316
+ <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
317
+ {invoice.Email}
318
+ </Typography>
282
319
  )}
283
320
  {invoice.BillingAddress && (
284
- <Typography variant="body2">
321
+ <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
285
322
  {invoice.BillingAddress}
286
323
  </Typography>
287
324
  )}
288
325
  </Grid2>
289
326
  )}
290
-
291
327
  {invoice?.InvoiceDate && (
292
- <Grid2 size={6}>
328
+ <Grid2 size={{ xs: 12, sm: 6 }}>
293
329
  <Typography variant="body2" color="text.secondary">
294
330
  Invoice Date
295
331
  </Typography>
296
332
  <Typography>{formatDate(invoice.InvoiceDate)}</Typography>
297
333
  </Grid2>
298
334
  )}
299
-
300
335
  {invoice?.DueDate && (
301
- <Grid2 size={6}>
336
+ <Grid2 size={{ xs: 12, sm: 6 }}>
302
337
  <Typography variant="body2" color="text.secondary">
303
338
  Due Date
304
339
  </Typography>
@@ -306,116 +341,114 @@ export const InvoiceDetailsTemplate = ({
306
341
  </Grid2>
307
342
  )}
308
343
  </Grid2>
309
-
310
- <Divider sx={{ my: 3 }} />
344
+ <Divider sx={{ my: { xs: 2, sm: 3 } }} />
311
345
  </>
312
346
  )}
313
347
 
314
348
  {/* Line Items Section */}
315
349
  {items.length > 0 ? (
316
350
  <>
317
- {/* Table Header */}
318
- <Grid2 container sx={{ mb: 1 }}>
319
- {columns.map((col) => (
320
- <Grid2 key={col.label} size={col.size}>
321
- <Typography variant="body2" fontWeight={600}>
322
- {col.label}
323
- </Typography>
324
- </Grid2>
325
- ))}
326
- </Grid2>
327
-
328
- {/* Line Items */}
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;
334
-
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>
346
-
347
- <Grid2 size={3.5}>
348
- <Typography variant="body2">{productName}</Typography>
349
- </Grid2>
350
-
351
- <Grid2 size={1}>
352
- <Typography variant="body2">{quantity}</Typography>
353
- </Grid2>
354
-
355
- <Grid2 size={1}>
356
- <Typography variant="body2">
357
- {item?.UnitOfMeasure || "—"}
358
- </Typography>
359
- </Grid2>
360
-
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>
377
-
378
- <Grid2 size={1.5}>
379
- <Typography variant="body2" fontWeight={500}>
380
- {formatCurrency(lineTotal)}
381
- </Typography>
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>
396
- </Grid2>
397
- );
398
- })}
351
+ <TableContainer>
352
+ <Table sx={{ minWidth: totalTableWidth }}>
353
+ <TableHead>
354
+ <TableRow>
355
+ {columns.map((col) => (
356
+ <TableCell
357
+ key={col.label}
358
+ sx={{
359
+ width: col.width,
360
+ minWidth: col.width,
361
+ fontWeight: 600,
362
+ backgroundColor: theme.palette.background.paper,
363
+ }}
364
+ >
365
+ <Typography variant="body2" fontWeight={600}>
366
+ {col.label}
367
+ </Typography>
368
+ </TableCell>
369
+ ))}
370
+ </TableRow>
371
+ </TableHead>
372
+ <TableBody>
373
+ {items.map((item, idx) => {
374
+ const productName = item.ProductName || "-";
375
+ const unitPrice = item.Price ?? 0;
376
+ const quantity = item.Quantity ?? 0;
377
+ const lineTotal = item.NetAmount ?? unitPrice * quantity;
378
+
379
+ return (
380
+ <TableRow key={`${item.ItemId}-${idx}`}>
381
+ <TableCell sx={{ width: 60, minWidth: 60 }}>
382
+ <Typography variant="body2">
383
+ {item?.ItemId || idx + 1}
384
+ </Typography>
385
+ </TableCell>
386
+ <TableCell sx={{ width: 200, minWidth: 200 }}>
387
+ <Typography variant="body2">{productName}</Typography>
388
+ </TableCell>
389
+ <TableCell sx={{ width: 60, minWidth: 60 }}>
390
+ <Typography variant="body2">{quantity}</Typography>
391
+ </TableCell>
392
+ <TableCell sx={{ width: 60, minWidth: 60 }}>
393
+ <Typography variant="body2">
394
+ {item?.UnitOfMeasure || "—"}
395
+ </Typography>
396
+ </TableCell>
397
+ <TableCell sx={{ width: 120, minWidth: 120 }}>
398
+ <Typography variant="body2">
399
+ {formatCurrency(unitPrice)}
400
+ </Typography>
401
+ </TableCell>
402
+ <TableCell sx={{ width: 100, minWidth: 100 }}>
403
+ <Typography
404
+ variant="body2"
405
+ color={
406
+ item.HasDiscount ? "error.main" : "text.secondary"
407
+ }
408
+ >
409
+ {item.HasDiscount
410
+ ? `-${formatCurrency(item.DiscountAmount)}`
411
+ : formatCurrency(0)}
412
+ </Typography>
413
+ </TableCell>
414
+ <TableCell sx={{ width: 100, minWidth: 100 }}>
415
+ <Typography variant="body2" fontWeight={500}>
416
+ {formatCurrency(lineTotal)}
417
+ </Typography>
418
+ </TableCell>
419
+ <TableCell
420
+ sx={{ width: 40, minWidth: 40 }}
421
+ align="center"
422
+ >
423
+ {item.HasDiscount && (
424
+ <Typography color="error.main" fontWeight={700}>
425
+ *
426
+ </Typography>
427
+ )}
428
+ </TableCell>
429
+ </TableRow>
430
+ );
431
+ })}
432
+ </TableBody>
433
+ </Table>
434
+ </TableContainer>
399
435
 
400
436
  {/* Totals */}
401
437
  <Box mt={3} display="flex" justifyContent="flex-end">
402
- <Box width={300}>
438
+ <Box width={{ xs: "100%", sm: 300 }}>
403
439
  <Box display="flex" justifyContent="space-between" mb={1}>
404
440
  <Typography variant="body2">Subtotal</Typography>
405
441
  <Typography variant="body2" fontWeight={500}>
406
442
  {formatCurrency(subtotal)}
407
443
  </Typography>
408
444
  </Box>
409
-
410
445
  <Box display="flex" justifyContent="space-between" mb={1}>
411
446
  <Typography variant="body2">Tax</Typography>
412
447
  <Typography variant="body2" fontWeight={500}>
413
448
  {formatCurrency(tax)}
414
449
  </Typography>
415
450
  </Box>
416
-
417
451
  <Divider sx={{ my: 1.5 }} />
418
-
419
452
  <Box display="flex" justifyContent="space-between" mt={1}>
420
453
  <Typography variant="body1" fontWeight={700}>
421
454
  Total
@@ -1,8 +1,4 @@
1
- import type { Table } from "src/features/table";
2
- import type {
3
- StatusColorMap,
4
- StatusTextMap,
5
- } from "src/features/table/types/schemas";
1
+ import type * as Types from "../../../../../types";
6
2
  import { InvoiceDetailsTemplate } from "./invoice-details";
7
3
 
8
4
  /**
@@ -12,10 +8,10 @@ import { InvoiceDetailsTemplate } from "./invoice-details";
12
8
  */
13
9
  export interface DetailsTemplateProps {
14
10
  data: unknown;
15
- statusColorMap: StatusColorMap;
16
- statusTextMap: StatusTextMap;
11
+ statusColorMap: Types.Schemas.StatusColorMap;
12
+ statusTextMap: Types.Schemas.StatusTextMap;
17
13
  loading?: boolean;
18
- actions?: Table.Schemas.UIAction[];
14
+ actions?: Types.Schemas.UIAction[];
19
15
  resourceName?: string;
20
16
  }
21
17