@evenicanpm/portal-table-ui 2.2.3 → 2.3.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": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -36,16 +36,16 @@
36
36
  "@emotion/styled": "^11.11.0",
37
37
  "@mui/icons-material": "^7.3.6",
38
38
  "@mui/material": "^6.1.7",
39
+ "@tanstack/react-query": "^5.90.11",
39
40
  "@types/react": "^18.3.24",
40
41
  "typescript": "^5.6.3"
41
42
  },
42
43
  "dependencies": {
43
44
  "@arrows/multimethod": "^2.1.0",
44
- "@evenicanpm/portal-types-schemas": "^2.0.0",
45
45
  "@evenicanpm/ui": "^2.0.0",
46
46
  "@mui/lab": "^7.0.1-beta.19",
47
- "@tanstack/react-query": "^5.90.11",
48
- "date-fns": "^4.1.0"
47
+ "date-fns": "^4.1.0",
48
+ "zod": "^4.4.3"
49
49
  },
50
- "gitHead": "a95683f653c0d36518820f6f69e38009446ee138"
50
+ "gitHead": "72bf1aeb02a3d96c587706c0d91606698e38ff6a"
51
51
  }
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { method, multi } from "@arrows/multimethod";
3
- import type { Column } from "@evenicanpm/portal-types-schemas";
4
3
  import { FlexBox, H6, Subtitle } from "@evenicanpm/ui";
5
4
  import ArrowBackIcon from "@mui/icons-material/ArrowBack";
6
5
  import { Box, Button } from "@mui/material";
7
6
  import Grid from "@mui/material/Grid2";
8
7
  import { resolvers, transformers } from "../../../../logic";
9
8
  import type * as Types from "../../../../types";
9
+ import type { Column } from "../../../../types";
10
10
  import type { StatusColorMap, StatusTextMap } from "../../../../types/schemas";
11
11
  import { StatusPill } from "../status-pill";
12
12
  import { DetailsRowLabel } from "./label";
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
 
3
- import type { Column } from "@evenicanpm/portal-types-schemas";
4
3
  import {
5
4
  Table,
6
5
  TableBody,
@@ -10,6 +9,7 @@ import {
10
9
  TableRow,
11
10
  } from "@mui/material";
12
11
  import { resolvers } from "../../../../../logic";
12
+ import type { Column } from "../../../../../types";
13
13
  import type { DataMaskMap } from "../../../../../types/handlers";
14
14
 
15
15
  interface Props {
@@ -55,7 +55,6 @@ interface InvoiceLine {
55
55
  Price?: number;
56
56
  NetAmount?: number;
57
57
  UnitOfMeasure?: string;
58
- LineDiscount?: number;
59
58
  DiscountAmount: number;
60
59
  HasDiscount: boolean;
61
60
  }
@@ -67,6 +66,7 @@ interface InvoiceData {
67
66
  InvoiceSubtotal?: number;
68
67
  TotalTaxAmount?: number;
69
68
  Amount?: number;
69
+ AmountBalance?: number;
70
70
  CompanyName?: string;
71
71
  CompanyEmail?: string;
72
72
  CompanyAddress?: string;
@@ -143,10 +143,10 @@ export const InvoiceDetailsTemplate = ({
143
143
  const columns: TableColumn[] = [
144
144
  { label: "#", width: 60, aligned: "left" },
145
145
  { label: "Product", width: 200, aligned: "left" },
146
- { label: "Qty", width: 60, aligned: "right" },
146
+ { label: "Qty", width: 40, aligned: "right" },
147
147
  { label: "Unit", width: 60, aligned: "right" },
148
148
  { label: "Unit Price", width: 120, aligned: "right" },
149
- { label: "Discount", width: 100, aligned: "right" },
149
+ { label: "Line Savings", width: 120, aligned: "right" },
150
150
  { label: "Line Total", width: 120, aligned: "right" },
151
151
  { label: "", width: 40, aligned: "center" },
152
152
  ];
@@ -577,6 +577,25 @@ export const InvoiceDetailsTemplate = ({
577
577
  {formatCurrency(total)}
578
578
  </Typography>
579
579
  </Box>
580
+
581
+ {/* Paid and Remaining lines */}
582
+ {invoice?.Status !== 1 && (
583
+ <Box display="flex" justifyContent="space-between" mt={1}>
584
+ <Typography variant="body2">Paid</Typography>
585
+ <Typography variant="body2">
586
+ {formatCurrency(total - (invoice?.AmountBalance ?? 0))}
587
+ </Typography>
588
+ </Box>
589
+ )}
590
+
591
+ {invoice?.Status !== 1 && (invoice?.AmountBalance ?? 0) > 0 && (
592
+ <Box display="flex" justifyContent="space-between" mt={0.5}>
593
+ <Typography variant="body2">Remaining</Typography>
594
+ <Typography variant="body2">
595
+ {formatCurrency(invoice?.AmountBalance ?? 0)}
596
+ </Typography>
597
+ </Box>
598
+ )}
580
599
  </Box>
581
600
  </Box>
582
601
  </>
@@ -1,10 +1,10 @@
1
- import type { Column } from "@evenicanpm/portal-types-schemas";
2
1
  import { Skeleton } from "@mui/material";
3
2
  import Checkbox from "@mui/material/Checkbox";
4
3
  import { styled } from "@mui/material/styles";
5
4
  import TableCell from "@mui/material/TableCell";
6
5
  import TableHead from "@mui/material/TableHead";
7
6
  import TableRow from "@mui/material/TableRow";
7
+ import type { Column } from "../../../types";
8
8
 
9
9
  // STYLED COMPONENTS
10
10
  const StyledTableCell = styled(TableCell)(({ theme }) => ({
@@ -1,7 +1,7 @@
1
- import type { Column } from "@evenicanpm/portal-types-schemas";
2
1
  import { SearchInput } from "@evenicanpm/ui";
3
2
  import { MenuItem, Select } from "@mui/material";
4
3
  import React from "react";
4
+ import type { Column } from "../../../types";
5
5
 
6
6
  interface SearchProps {
7
7
  searchTerm: string;
@@ -84,14 +84,16 @@ const SearchField = React.memo(
84
84
  : "Select a column to search"
85
85
  }
86
86
  endAdornment={
87
- <ColumnSelect
88
- selected={selectedColumn?.Data || ""}
89
- searchCols={searchCols}
90
- handleChange={(value) => {
91
- const col = searchCols.find((c) => c.Data === value);
92
- if (col) setColumn(col);
93
- }}
94
- />
87
+ searchCols.length > 1 ? (
88
+ <ColumnSelect
89
+ selected={selectedColumn?.Data || ""}
90
+ searchCols={searchCols}
91
+ handleChange={(value) => {
92
+ const col = searchCols.find((c) => c.Data === value);
93
+ if (col) setColumn(col);
94
+ }}
95
+ />
96
+ ) : undefined
95
97
  }
96
98
  />
97
99
  );
@@ -1,4 +1,3 @@
1
- import type { Column } from "@evenicanpm/portal-types-schemas";
2
1
  import { FlexBox, ResponsiveTablePagination } from "@evenicanpm/ui";
3
2
  import Table from "@mui/material/Table";
4
3
  import TableBody from "@mui/material/TableBody";
@@ -10,6 +9,7 @@ import type React from "react";
10
9
  import { useState } from "react";
11
10
  import { resolvers, transformers } from "../../../logic";
12
11
  import type * as Types from "../../../types";
12
+ import type { Column } from "../../../types";
13
13
  import { BulkActionButton } from "./bulk-action-button";
14
14
  import { dataMaskMap as dataMaskMapLocal } from "./data-mask-map";
15
15
  import { Details } from "./details";
@@ -49,23 +49,25 @@ export const TableDashboard = ({
49
49
  return (
50
50
  <Box sx={{ width: "100%", minHeight: "70vh" }}>
51
51
  <TabContext value={value}>
52
- <Box
53
- sx={{
54
- borderBottom: 1,
55
- borderColor: "divider",
56
- "@media print": { display: "none" },
57
- }}
58
- >
59
- <TabList onChange={handleChange}>
60
- {resources?.map((resource, i) => (
61
- <Tab
62
- key={resource?.name + String(i)}
63
- label={resource?.label}
64
- value={resource?.name}
65
- />
66
- ))}
67
- </TabList>
68
- </Box>
52
+ {resources?.length > 1 && (
53
+ <Box
54
+ sx={{
55
+ borderBottom: 1,
56
+ borderColor: "divider",
57
+ "@media print": { display: "none" },
58
+ }}
59
+ >
60
+ <TabList onChange={handleChange}>
61
+ {resources?.map((resource, i) => (
62
+ <Tab
63
+ key={resource?.name + String(i)}
64
+ label={resource?.label}
65
+ value={resource?.name}
66
+ />
67
+ ))}
68
+ </TabList>
69
+ </Box>
70
+ )}
69
71
  {resources?.map((resource, i) => (
70
72
  <Box key={resource.name + String(i)}>
71
73
  <TabPanel
@@ -1,5 +1,4 @@
1
1
  import { method, multi } from "@arrows/multimethod";
2
- import type { Column } from "@evenicanpm/portal-types-schemas";
3
2
  import type * as Types from "../types";
4
3
  import type { TableConfig } from "../types/queries";
5
4
  import type { UIAction, UIBulkAction } from "../types/schemas";
@@ -37,7 +36,7 @@ export const resolveFilterUI = multi(
37
36
  * @internal
38
37
  */
39
38
  export const resolveDataMask = multi(
40
- (col: Column, handlers: Types.Handlers.DataMaskMap) => {
39
+ (col: Types.Column, handlers: Types.Handlers.DataMaskMap) => {
41
40
  if (!col?.DataMask) return "SKIP";
42
41
  if (handlers?.[col?.DataType]) return "BY_TYPE";
43
42
  if (handlers?.[col?.Data]) return "BY_NAME";
@@ -45,12 +44,12 @@ export const resolveDataMask = multi(
45
44
  },
46
45
  method(
47
46
  "BY_TYPE",
48
- (col: Column, handlers: Types.Handlers.DataMaskMap) =>
47
+ (col: Types.Column, handlers: Types.Handlers.DataMaskMap) =>
49
48
  handlers?.[col?.DataType],
50
49
  ),
51
50
  method(
52
51
  "BY_NAME",
53
- (col: Column, handlers: Types.Handlers.DataMaskMap) =>
52
+ (col: Types.Column, handlers: Types.Handlers.DataMaskMap) =>
54
53
  handlers?.[col?.Data],
55
54
  ),
56
55
  method("SKIP", () => (value: string | number) => value),
@@ -61,7 +60,7 @@ export const resolveDataMask = multi(
61
60
  * @internal
62
61
  */
63
62
  export const resolveCellUI = (
64
- col: Column,
63
+ col: Types.Column,
65
64
  handlers: Types.Handlers.CellRenderMap,
66
65
  ) => {
67
66
  if (Object.hasOwn(handlers, col.Data)) return handlers?.[col?.Data];
@@ -1,4 +1,4 @@
1
- import type { Column } from "@evenicanpm/portal-types-schemas";
1
+ import type { Column } from "../types";
2
2
 
3
3
  /**
4
4
  * These are pure static functions that include business rules
@@ -1,4 +1,4 @@
1
- import type { FilterOption } from "@evenicanpm/portal-types-schemas";
1
+ import type { FilterOption } from "../types";
2
2
 
3
3
  /**
4
4
  *
@@ -68,3 +68,5 @@
68
68
  export * as Handlers from "./handlers";
69
69
  export * as Queries from "./queries";
70
70
  export * as Schemas from "./schemas";
71
+ export * from "./table-param";
72
+ export * from "./view-config";
@@ -1,4 +1,4 @@
1
- import type { ViewConfig } from "@evenicanpm/portal-types-schemas";
1
+ import type { ViewConfig } from "../types";
2
2
 
3
3
  /**
4
4
  *
@@ -1,8 +1,8 @@
1
- import type { TableViewParams } from "@evenicanpm/portal-types-schemas";
2
1
  import type { SvgIconComponent } from "@mui/icons-material";
3
2
  import type { UseMutationOptions } from "@tanstack/react-query";
4
3
  import type { Table } from "..";
5
4
  import type { DetailsTemplateKey } from "../components/dashboard/table/details/templates/template-registry";
5
+ import type { TableViewParams } from "../types";
6
6
  import type {
7
7
  CellRenderMap,
8
8
  DataMaskMap,
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const TableViewParamsSchema = z.object({
4
+ pageSize: z.number().default(5),
5
+ page: z.number().default(0),
6
+ filters: z.record(z.string(), z.unknown()).optional(),
7
+ searchTerm: z.string().optional().default(""),
8
+ searchColumn: z.string().default("all"),
9
+ });
10
+
11
+ // Extract the TypeScript interface from the schema
12
+ export type TableViewParams = z.infer<typeof TableViewParamsSchema>;
13
+
14
+ /**
15
+ * convert the table state into url params.
16
+ * @param params Filters and params stored from table state
17
+ * @returns Url params.
18
+ */
19
+ export const tableStateToQuery = (params: TableViewParams): URLSearchParams => {
20
+ const { page, pageSize, searchTerm, searchColumn, filters } = params;
21
+ const searchParams = new URLSearchParams();
22
+ searchParams.set("page", String(page));
23
+ searchParams.set("pageSize", String(pageSize));
24
+
25
+ if (searchTerm && searchColumn) {
26
+ searchParams.set("searchTerm", searchTerm);
27
+ searchParams.set("searchColumn", searchColumn);
28
+ }
29
+
30
+ if (filters) searchParams.set("filters", JSON.stringify(filters));
31
+ return searchParams;
32
+ };
33
+
34
+ /**
35
+ * Pre-processes url params into portal format to be
36
+ * inserted into one of the DMS URL builders
37
+ *
38
+ * @param searchParams url params from next endpoint
39
+ * @returns table view params formatted filters/pagination
40
+ */
41
+ export const queryToTableState = (
42
+ searchParams: URLSearchParams,
43
+ ): TableViewParams => {
44
+ const page = Number(searchParams.get("page") ?? 0);
45
+ const pageSize = Number(searchParams.get("pageSize") ?? 5);
46
+ const searchTerm = searchParams.get("searchTerm") ?? "";
47
+ const searchColumn = searchParams.get("searchColumn") ?? "";
48
+ const filtersValue = searchParams.get("filters");
49
+ const filters = filtersValue && JSON.parse(filtersValue);
50
+ return { page, pageSize, searchColumn, searchTerm, filters };
51
+ };
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * ColumnDataType
4
+ */
5
+ export const ColumnDataTypeSchema = z.enum([
6
+ "String",
7
+ "int",
8
+ "numeric",
9
+ "date",
10
+ "datetime",
11
+ "url",
12
+ ]);
13
+ export type ColumnDataType = z.infer<typeof ColumnDataTypeSchema>;
14
+
15
+ /**
16
+ * DataMaskFormat
17
+ */
18
+ export const DataMaskFormatSchema = z.enum([
19
+ "MM/DD/YYYY",
20
+ "MM-DD-YYYY",
21
+ "DD/MM/YYYY",
22
+ "DD-MM-YYYY",
23
+ "YYYY/MM/DD",
24
+ "YYYY-MM-DD HH:MI:SS",
25
+ "$0.00",
26
+ "0,00€",
27
+ ]);
28
+ export type DataMaskFormat = z.infer<typeof DataMaskFormatSchema>;
29
+
30
+ /**
31
+ * FilterOperation
32
+ */
33
+ export const FilterOperationSchema = z.enum([
34
+ "eq",
35
+ "gt",
36
+ "lt",
37
+ "gte",
38
+ "lte",
39
+ "in",
40
+ ]);
41
+ export type FilterOperation = z.infer<typeof FilterOperationSchema>;
42
+
43
+ /**
44
+ * FilterOption
45
+ */
46
+ export const FilterOptionSchema = z.object({
47
+ value: z.union([z.string(), z.number()]),
48
+ label: z.string(),
49
+ });
50
+ export type FilterOption = z.infer<typeof FilterOptionSchema>;
51
+
52
+ /**
53
+ * FilterConfig
54
+ */
55
+ export const FilterConfigSchema = z.object({
56
+ FilterOptions: z.array(FilterOptionSchema),
57
+ FilterType: z.string(),
58
+ FilterOperation: FilterOperationSchema,
59
+ });
60
+ export type FilterConfig = z.infer<typeof FilterConfigSchema>;
61
+
62
+ /**
63
+ * Filter (from table metadata response)
64
+ */
65
+ export const FilterSchema = z.object({
66
+ Operation: FilterOperationSchema,
67
+ Data: z.string(),
68
+ Value: z.union([z.string(), z.array(z.string()), z.number()]),
69
+ });
70
+ export type Filter = z.infer<typeof FilterSchema>;
71
+
72
+ /**
73
+ * Column (recursive)
74
+ */
75
+ export const ColumnSchema = z.object({
76
+ Label: z.string(),
77
+ Data: z.string(),
78
+ DataType: z.string(),
79
+ DataMask: z.string().optional(),
80
+ ColumnOrder: z.number().nullable().optional(),
81
+ IsSearchable: z.boolean().optional(),
82
+ IsFilterable: z.boolean().optional(),
83
+ Filters: z.array(FilterConfigSchema).optional(),
84
+ IsVisible: z.boolean().optional(),
85
+ IsSortable: z.boolean().optional(),
86
+ DisplayOrder: z.number().optional(),
87
+ });
88
+
89
+ export type Action = {
90
+ Name: string;
91
+ Type: string;
92
+ IdField?: string;
93
+ IsEnabled: boolean;
94
+ };
95
+
96
+ export type BulkAction = {
97
+ Name: string;
98
+ Type: string;
99
+ IdField?: string;
100
+ IsEnabled: boolean;
101
+ };
102
+
103
+ export interface Column {
104
+ Label: string;
105
+ Data: string;
106
+ DataType: string;
107
+ ColumnOrder: number | undefined | null;
108
+ DataMask?: string;
109
+ IsSearchable?: boolean;
110
+ IsFilterable?: boolean;
111
+ Filters?: FilterConfig[];
112
+ IsVisible?: boolean;
113
+ IsSortable?: boolean;
114
+ DisplayOrder?: number;
115
+ Children?: Column[];
116
+ }
117
+
118
+ export type ViewConfig = {
119
+ Columns: Column[];
120
+ Actions?: Action[];
121
+ BulkActions?: BulkAction[];
122
+ };