diff --git a/package-lock.json b/package-lock.json index 451971a4..d4e757c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", "@types/styled-components": "^5.1.34", + "@uidotdev/usehooks": "^2.4.1", "chia-dat-seeder": "^1.0.1", "chia-datalayer": "^2.0.17", "chia-datalayer-fs-deploy": "^1.0.15", @@ -2363,6 +2364,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", diff --git a/src/renderer/api/cadt/v1/issuances/issuances.api.ts b/src/renderer/api/cadt/v1/issuances/issuances.api.ts index ff716b83..86ceb37b 100644 --- a/src/renderer/api/cadt/v1/issuances/issuances.api.ts +++ b/src/renderer/api/cadt/v1/issuances/issuances.api.ts @@ -1,5 +1,5 @@ -import { cadtApi, issuancesTag } from ".."; -import { Issuance } from "@/schemas/Issuance.schema"; +import { cadtApi, issuancesTag } from '..'; +import { Issuance } from '@/schemas/Issuance.schema'; interface GetIssuancesParams { issuanceIds: string[]; @@ -7,31 +7,23 @@ interface GetIssuancesParams { const issuanceApi = cadtApi.injectEndpoints({ endpoints: (builder) => ({ - getIssuances: builder.query({ + getIssuances: builder.query({ query: ({ issuanceIds }: GetIssuancesParams) => ({ url: `/v1/issuances`, params: { issuanceIds }, method: 'GET', }), - // Adding error handling in providesTags - providesTags: (result, error) => { - // Check if there was an error or if the result is undefined or null - if (error || !result) { - // Handle errors or no result by returning a minimal tag - // This prevents caching potentially faulty data - return [{ type: issuancesTag, id: 'ERROR' }]; + // @ts-ignore + providesTags: (result) => { + if (result) { + return [ + ...result.map((issuance) => ({ type: issuancesTag, id: issuance.id })), + { type: issuancesTag, id: 'PARTIAL-LIST' }, // Additional tag for managing partial lists + ]; } - - // Provide a tag for each issuance returned by the query - return [ - ...result.map(issuance => ({ type: issuancesTag, id: issuance.id })), - { type: issuancesTag, id: 'PARTIAL-LIST' } // Additional tag for managing partial lists - ]; - } + }, }), - }) + }), }); -export const { - useGetIssuancesQuery -} = issuanceApi; +export const { useGetIssuancesQuery } = issuanceApi; diff --git a/src/renderer/api/cadt/v1/units/units.api.ts b/src/renderer/api/cadt/v1/units/units.api.ts index e0e0777f..f5e551c2 100644 --- a/src/renderer/api/cadt/v1/units/units.api.ts +++ b/src/renderer/api/cadt/v1/units/units.api.ts @@ -6,6 +6,7 @@ interface GetUnitsParams { orgUid?: string; search?: string; order?: string; + filter?: string; } interface GetUnitParams { @@ -21,7 +22,7 @@ interface GetUnitsResponse { const unitsApi = cadtApi.injectEndpoints({ endpoints: (builder) => ({ getUnits: builder.query({ - query: ({ page, orgUid, search, order }: GetUnitsParams) => { + query: ({ page, orgUid, search, order, filter }: GetUnitsParams) => { // Initialize the params object with page and limit const params: GetUnitsParams & {limit: number} = { page, limit: 10 }; @@ -37,6 +38,10 @@ const unitsApi = cadtApi.injectEndpoints({ params.order = order; } + if (filter) { + params.filter = filter; + } + return { url: `/v1/units`, params, diff --git a/src/renderer/components/blocks/forms/IssuanceForm.tsx b/src/renderer/components/blocks/forms/IssuanceForm.tsx index 2d5a7c49..4a097ca4 100644 --- a/src/renderer/components/blocks/forms/IssuanceForm.tsx +++ b/src/renderer/components/blocks/forms/IssuanceForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Form, Formik } from 'formik'; import * as yup from 'yup'; -import { Field, Repeater } from '@/components'; +import { Field, Repeater, UnitSummary } from '@/components'; import { Issuance } from '@/schemas/Issuance.schema'; const validationSchema = yup.object({ @@ -20,9 +20,10 @@ interface IssuanceFormProps { onSubmit: (values: any) => Promise; readonly?: boolean; data?: Issuance[] | undefined; + showUnits?: boolean; } -const IssuanceForm: React.FC = ({ readonly = false, data }) => { +const IssuanceForm: React.FC = ({ readonly = false, data, showUnits = false }) => { return ( = ({ readonly = false, data }) = readonly={readonly} initialValue={data || []} > - - - - - + {(group) => ( + <> + + + + + +
+ {showUnits && readonly && group.id && ( +
+
Unit Belonging to this Issuance
+ + +
+ )} + + )} )} diff --git a/src/renderer/components/blocks/modals/ProjectModal.tsx b/src/renderer/components/blocks/modals/ProjectModal.tsx index d552198c..0bdd9253 100644 --- a/src/renderer/components/blocks/modals/ProjectModal.tsx +++ b/src/renderer/components/blocks/modals/ProjectModal.tsx @@ -30,21 +30,26 @@ const ProjectModal: React.FC = ({ warehouseProjectId, onClose )} - }> - {!isLoading && data && ( - - )} - - }> - {!isLoading && data && ( + {data?.issuances?.length && ( + }> + + + )} + {data?.projectLocations?.length && ( + }> - )} - - }> - {!isLoading && data && ( + + )} + {data?.estimations?.length && ( + }> - )} - + + )} diff --git a/src/renderer/components/blocks/modals/UnitModal.tsx b/src/renderer/components/blocks/modals/UnitModal.tsx index 20107c95..ed9ca1b7 100644 --- a/src/renderer/components/blocks/modals/UnitModal.tsx +++ b/src/renderer/components/blocks/modals/UnitModal.tsx @@ -30,11 +30,11 @@ const UnitModal: React.FC = ({ warehouseUnitId, onClose }: UnitM )} - }> - {!unitLoading && unitData && ( + {unitData?.issuance && ( + }> - )} - + + )} diff --git a/src/renderer/components/blocks/tables/CompactUnitsTable.tsx b/src/renderer/components/blocks/tables/CompactUnitsTable.tsx new file mode 100644 index 00000000..9d94ed22 --- /dev/null +++ b/src/renderer/components/blocks/tables/CompactUnitsTable.tsx @@ -0,0 +1,75 @@ +import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { DataTable, PageCounter, Pagination } from '@/components'; +import { DebouncedFunc } from 'lodash'; +import { Unit } from '@/schemas/Unit.schema'; + +interface TableProps { + data: Unit[]; + currentPage: number; + onPageChange: DebouncedFunc<(page: any) => void>; + totalPages: number; + onRowClick?: (row: any) => void; + totalCount: number; +} + +const CompactUnitsTable: React.FC = ({ + data, + currentPage, + onPageChange, + totalPages, + totalCount, + onRowClick, +}) => { + const columns = useMemo( + () => [ + { + title: , + key: 'serialNumberBlock', + }, + { + title: , + key: 'unitCount', + }, + { + title: , + key: 'unitOwner', + render: (row: Unit) => {row.unitOwner || '-'}, + }, + ], + [], + ); + + const shouldRenderPagination = useMemo(() => totalPages > 1 && Boolean(data), [totalPages, data]); + + return ( +
+ + {shouldRenderPagination ? ( + <> + + + + ) : null} +
+ } + /> +
+ ); +}; + +export { CompactUnitsTable }; diff --git a/src/renderer/components/blocks/tables/index.ts b/src/renderer/components/blocks/tables/index.ts index 572cefaa..16a7fe5f 100644 --- a/src/renderer/components/blocks/tables/index.ts +++ b/src/renderer/components/blocks/tables/index.ts @@ -1,5 +1,6 @@ -export * from './ProjectsListTable'; -export * from './SkeletonTable'; -export * from './UnitsListTable'; -export * from './AuditsTable'; -export * from './GlossaryTable'; \ No newline at end of file +export * from './ProjectsListTable'; +export * from './SkeletonTable'; +export * from './UnitsListTable'; +export * from './AuditsTable'; +export * from './GlossaryTable'; +export * from './CompactUnitsTable'; \ No newline at end of file diff --git a/src/renderer/components/blocks/widgets/UnitSummary.tsx b/src/renderer/components/blocks/widgets/UnitSummary.tsx new file mode 100644 index 00000000..f51a69b3 --- /dev/null +++ b/src/renderer/components/blocks/widgets/UnitSummary.tsx @@ -0,0 +1,58 @@ +import { debounce } from 'lodash'; +import React, { useState, useCallback } from 'react'; +import { CompactUnitsTable, Spinner } from '@/components'; +import { useGetUnitsQuery } from '@/api'; +import { useNavigate } from 'react-router-dom'; + +const UnitSummary: React.FC<{ issuanceId: any }> = ({ issuanceId }) => { + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + + // Retrieve the query hook including the abort function + const { + data: units, + isLoading, + isFetching, + } = useGetUnitsQuery({ + filter: `issuanceId:${issuanceId}:eq`, + page: currentPage, + }); + + const handlePageChange = useCallback( + debounce((page) => { + setCurrentPage(page); + }, 800), + [setCurrentPage], + ); + + const onRowClick = useCallback( + (row: any) => { + // Ensure the Unit type is defined or use any here + navigate(`/units#unit-${row.warehouseUnitId}`); + }, + [navigate], + ); + + if (isLoading || isFetching) { + return ; + } + + if (!units) { + return
No units found
; + } + + return ( + <> + + + ); +}; + +export { UnitSummary }; diff --git a/src/renderer/components/blocks/widgets/index.ts b/src/renderer/components/blocks/widgets/index.ts index 112003cc..39745394 100644 --- a/src/renderer/components/blocks/widgets/index.ts +++ b/src/renderer/components/blocks/widgets/index.ts @@ -2,4 +2,5 @@ export * from './SearchBox'; export * from './OrganizationSelector'; export * from './SyncIndicator'; export * from './OrgUidBadge'; -export * from './ThemeSelector'; \ No newline at end of file +export * from './ThemeSelector'; +export * from './UnitSummary'; \ No newline at end of file diff --git a/src/renderer/components/form/Field.tsx b/src/renderer/components/form/Field.tsx index 86beb592..03cb3b2c 100644 --- a/src/renderer/components/form/Field.tsx +++ b/src/renderer/components/form/Field.tsx @@ -34,7 +34,7 @@ const Field: React.FC = ({ name, label, type, options, readonly, ini return
{options?.find((option) => option.value === initialValue)?.label}
; case 'link': return ( - + {initialValue} ); diff --git a/src/renderer/components/form/Repeater.tsx b/src/renderer/components/form/Repeater.tsx index 3fb4f017..876291d9 100644 --- a/src/renderer/components/form/Repeater.tsx +++ b/src/renderer/components/form/Repeater.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { ReactNode, Fragment, ReactElement, useCallback } from 'react'; import { FieldArray, FormikContextType, useFormikContext } from 'formik'; -import { Button, Card } from '@/components'; +import { Button, Card, Field } from '@/components'; import { IoAddCircleOutline } from 'react-icons/io5'; // Importing a plus icon from react-icons interface RepeaterProps { name: string; - children: React.ReactNode; + children: ReactNode | ((item: T, index: number) => ReactNode); maxNumber?: number; minNumber?: number; readonly?: boolean; @@ -30,38 +30,73 @@ function Repeater({ const groups = values[name] || initialValue; - const createBlankItem = (): T => { + const createBlankItem = useCallback((): T => { const blankItem = {} as T; React.Children.forEach(children, (child: any) => { if (child.props.name) { // @ts-ignore - blankItem[child.props.name as keyof T] = null; // Assign a default blank or appropriate value + blankItem[child.props.name as keyof T] = null; } }); return blankItem; - }; + }, [children]); + const renderChildren = useCallback((group: T, index: number): ReactNode => { + const childNodes = typeof children === 'function' ? children(group, index) : children; + const fieldElements: ReactNode[] = []; + const otherElements: ReactNode[] = []; + + const processChild = (child: ReactElement) => { + if (React.isValidElement(child)) { + if (child.type === Field) { + const clonedField = React.cloneElement(child, { + // @ts-ignore + key: `${child.props.name}-${index}`, + // @ts-ignore + name: `${name}[${index}].${child.props.name}`, + // @ts-ignore + initialValue: group[child.props.name], + readonly: readonly, + }); + fieldElements.push(clonedField); + } else { + otherElements.push(child); + } + } + }; + + React.Children.forEach(childNodes, (child) => { + if (React.isValidElement(child) && child.type === Fragment) { + React.Children.forEach(child.props.children, processChild); + } else { + // @ts-ignore + processChild(child); + } + }); + + return ( + <> +
+ {fieldElements} +
+ {otherElements} + + ); + }, [children, name, readonly]); + return ( (
{groups.map((_group: any, index: number) => ( - +
-
- {React.Children.map(children, (child: any) => { - return React.cloneElement(child, { - name: `${name}[${index}].${child.props.name}`, - initialValue: _group[child.props.name], - readonly: readonly, - }); - })} -
+
{renderChildren(_group, index)}
{!readonly && (