From ca346811d1ff945d90747b1b87f48ec52aca0c4d Mon Sep 17 00:00:00 2001 From: Vincenzo Pierro Date: Tue, 25 Feb 2025 19:37:27 +0100 Subject: [PATCH 1/4] :sparkles: Added downloadable material report --- src/pages/material/MaterialReportModal.tsx | 57 ++++++++++++++++++++++ src/pages/material/SearchMaterialsPage.tsx | 19 +++++++- src/services/material.ts | 12 ++++- src/utils/url-utils.ts | 3 ++ 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 src/pages/material/MaterialReportModal.tsx create mode 100644 src/utils/url-utils.ts diff --git a/src/pages/material/MaterialReportModal.tsx b/src/pages/material/MaterialReportModal.tsx new file mode 100644 index 0000000..4175c6a --- /dev/null +++ b/src/pages/material/MaterialReportModal.tsx @@ -0,0 +1,57 @@ +import { useIsMobileLayout } from '../../hooks/responsive-size' +import { + Button, + Center, + Icon, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Text, +} from '@chakra-ui/react' +import { FileArrowDown } from '@phosphor-icons/react' +import { useInitReportCreationMutation } from '../../services/material' +import { getReportDownloadUrl } from '../../utils/url-utils' + +interface MaterialReportModalProps { + onClose: () => void + isOpen: boolean +} + +export const MaterialReportModal = ({ onClose, isOpen }: MaterialReportModalProps) => { + const [createReport, { isLoading }] = useInitReportCreationMutation() + const handleDownload = async () => { + const result = await createReport() + if ('data' in result) { + window.open(getReportDownloadUrl(result.data), '_blank') + } + } + return ( + + + + + Download a report of the materials + + + +
+ +
+
+
+
+ ) +} diff --git a/src/pages/material/SearchMaterialsPage.tsx b/src/pages/material/SearchMaterialsPage.tsx index 4b3f255..8f7bccc 100644 --- a/src/pages/material/SearchMaterialsPage.tsx +++ b/src/pages/material/SearchMaterialsPage.tsx @@ -4,9 +4,12 @@ import { setPageTitle } from '../../store/ui/ui-slice' import { Alert, AlertIcon, + Button, Center, CloseButton, Container, + Icon, + IconButton, Input, InputGroup, InputLeftAddon, @@ -15,6 +18,7 @@ import { Skeleton, Spinner, Stack, + useDisclosure, VStack, } from '@chakra-ui/react' import { useGetTagsQuery } from '../../services/tag' @@ -31,6 +35,9 @@ import { StackedSkeleton } from '../../components/ui/StackedSkeleton' import { MaterialCard } from '../../components/models/MaterialCard' import { useIsMobileLayout } from '../../hooks/responsive-size' import { getIdsInPage } from '../../utils/array-utils' +import { getReportDownloadUrl } from '../../utils/url-utils' +import { FileXls } from '@phosphor-icons/react' +import { MaterialReportModal } from './MaterialReportModal' export const SearchMaterialsPage = () => { const isMobile = useIsMobileLayout() @@ -48,6 +55,7 @@ export const SearchMaterialsPage = () => { const [rawQuery, setRawQuery] = useState('') const [query, setQuery] = useState(undefined) const [isTyping, setIsTyping] = useState(false) + const { isOpen: reportModalIsOpen, onOpen: reportModalOpen, onClose: onReportModalClose } = useDisclosure() const { data: tags, error: tagsError, isLoading: tagsLoading } = useGetTagsQuery() const { @@ -62,7 +70,6 @@ export const SearchMaterialsPage = () => { } = useGetMaterialsByIdsQuery(getIdsInPage(materialIds, currentPage, pageSize), { skip: !materialIds || getIdsInPage(materialIds, currentPage, pageSize).length === 0, }) - const prefetchNextPage = useCallback(() => { const nextIds = getIdsInPage(materialIds, currentPage + 1, pageSize) if (nextIds.length > 0) { @@ -149,7 +156,7 @@ export const SearchMaterialsPage = () => { {isTyping && ( @@ -158,6 +165,13 @@ export const SearchMaterialsPage = () => { )} + } + onClick={reportModalOpen} + /> {!!idsError && ( { onNextEnter={prefetchNextPage} /> + ) } diff --git a/src/services/material.ts b/src/services/material.ts index ba05cdb..031cbe0 100644 --- a/src/services/material.ts +++ b/src/services/material.ts @@ -102,18 +102,26 @@ export const materialApi = createApi({ }), providesTags: materialTagProvider, }), + initReportCreation: builder.mutation({ + query: () => ({ + url: '/report', + method: 'POST', + responseHandler: 'text', + }), + }), }), }) export const { useCreateMaterialMutation, useDeleteMaterialMutation, + useFilterMaterialsQuery, + useFindMaterialsByFuzzyNameQuery, useGetLastCreatedQuery, useGetMaterialsByIdsQuery, useGetMaterialsByRefCodeQuery, useGetMaterialQuery, - useFilterMaterialsQuery, - useFindMaterialsByFuzzyNameQuery, + useInitReportCreationMutation, useModifyMaterialMutation, useSearchIdsByNameBrandCodeQuery, useSearchNamesByNameBrandCodeQuery, diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts new file mode 100644 index 0000000..0d310ef --- /dev/null +++ b/src/utils/url-utils.ts @@ -0,0 +1,3 @@ +export function getReportDownloadUrl(token: string): string { + return `${process.env.REACT_APP_APIURL}/material/report?token=${token}` +} From 02ccd177f75d6768fece0198cf7afdc697cea48a Mon Sep 17 00:00:00 2001 From: Vincenzo Pierro Date: Tue, 25 Feb 2025 20:01:31 +0100 Subject: [PATCH 2/4] :bug: Fixed select box issue on box update --- src/components/forms/UpdateQuantityForm.tsx | 5 +++++ src/components/forms/controls/BoxUnitSelector.tsx | 11 ++++++++++- src/pages/material/MaterialReportModal.tsx | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/forms/UpdateQuantityForm.tsx b/src/components/forms/UpdateQuantityForm.tsx index bc533e8..dbef2da 100644 --- a/src/components/forms/UpdateQuantityForm.tsx +++ b/src/components/forms/UpdateQuantityForm.tsx @@ -27,6 +27,10 @@ interface UpdateQuantityFormValues extends FormValues { export const UpdateQuantityForm = ({ box, boxDefinition, onDispatched }: UpdateQuantityFormProps) => { const unitSteps = useMemo(() => unitToStepsList(boxDefinition), [boxDefinition]) + const boxCount = useMemo( + () => Math.floor(box.quantity.quantity / unitSteps.reduce((p, c) => p * c.qty, 1)), + [box.quantity.quantity, unitSteps] + ) const initialState: UpdateQuantityFormValues = { date: { value: new Date().getTime(), isValid: true }, quantity: { @@ -95,6 +99,7 @@ export const UpdateQuantityForm = ({ box, boxDefinition, onDispatched }: UpdateQ /> !!input && !input.boxUnit && input.quantity > 0} valueConsumer={value => { diff --git a/src/components/forms/controls/BoxUnitSelector.tsx b/src/components/forms/controls/BoxUnitSelector.tsx index 2770e14..8ca5dcd 100644 --- a/src/components/forms/controls/BoxUnitSelector.tsx +++ b/src/components/forms/controls/BoxUnitSelector.tsx @@ -18,6 +18,7 @@ import { computeTotal } from '../../../utils/box-utils' interface BoxUnitSelectorProps extends LayoutProps, SpaceProps, HTMLChakraProps<'div'> { boxUnit: BoxUnit + boxCount?: number label: string validator?: (input?: BoxUnit) => boolean valueConsumer?: (value: FormValue) => void @@ -26,13 +27,21 @@ interface BoxUnitSelectorProps extends LayoutProps, SpaceProps, HTMLChakraProps< export const BoxUnitSelector = ({ boxUnit, + boxCount, label, validator, valueConsumer, invalidLabel, ...style }: BoxUnitSelectorProps) => { - const unitSteps = useMemo(() => unitToStepsList(boxUnit), [boxUnit]) + const unitSteps = useMemo(() => { + const steps = unitToStepsList(boxUnit) + if (boxCount != null) { + return [{ ...steps[0], qty: boxCount }, ...steps.slice(1)] + } else { + return steps + } + }, [boxUnit]) const [stepValues, setStepValues] = useState( unitSteps .slice(0, unitSteps.length - 1) diff --git a/src/pages/material/MaterialReportModal.tsx b/src/pages/material/MaterialReportModal.tsx index 4175c6a..154dbd6 100644 --- a/src/pages/material/MaterialReportModal.tsx +++ b/src/pages/material/MaterialReportModal.tsx @@ -1,4 +1,3 @@ -import { useIsMobileLayout } from '../../hooks/responsive-size' import { Button, Center, From 976f81210c02f9ebae8582ffaab11822ac286e55 Mon Sep 17 00:00:00 2001 From: Vincenzo Pierro Date: Tue, 25 Feb 2025 20:30:24 +0100 Subject: [PATCH 3/4] :sparkles: Allowed manual input in box quantity selector --- .../forms/controls/BoxUnitSelector.tsx | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/components/forms/controls/BoxUnitSelector.tsx b/src/components/forms/controls/BoxUnitSelector.tsx index 8ca5dcd..139fddc 100644 --- a/src/components/forms/controls/BoxUnitSelector.tsx +++ b/src/components/forms/controls/BoxUnitSelector.tsx @@ -41,7 +41,7 @@ export const BoxUnitSelector = ({ } else { return steps } - }, [boxUnit]) + }, [boxCount, boxUnit]) const [stepValues, setStepValues] = useState( unitSteps .slice(0, unitSteps.length - 1) @@ -83,6 +83,17 @@ export const BoxUnitSelector = ({ [dispatchNewQuantity, unitSteps] ) + const onSetValue = useCallback( + (idx: number, value: number) => { + setStepValues(previousState => { + const newQty = setState(idx, value, previousState, unitSteps) + dispatchNewQuantity(newQty) + return newQty + }) + }, + [dispatchNewQuantity, unitSteps] + ) + const onDecrease = useCallback( (idx: number) => { setStepValues(previousState => { @@ -103,7 +114,14 @@ export const BoxUnitSelector = ({ - + ) => { + onSetValue(idx, parseInt(event.currentTarget.value)) + }} + /> @@ -120,8 +138,21 @@ export const BoxUnitSelector = ({ ) } +function setState(idx: number, value: number, state: number[], steps: UnitStep[]): number[] { + const newValue = value < 0 ? 0 : value > steps[idx].qty ? steps[idx].qty : value + if (idx === 0 && newValue === steps[0].qty) { + return state.map((_, index) => (index === 0 ? newValue : 0)) + } else { + return state.map((element, index) => (index === idx ? newValue : element)) + } +} + function increaseState(idx: number, state: number[], steps: UnitStep[]): number[] { - if (state[0] === steps[0].qty) { + if (isNaN(state[idx])) { + const newState = [...state] + newState[idx] = 0 + return newState + } else if (state[0] === steps[0].qty) { const newState = state.map(_ => 0) newState[0] = state[0] return newState @@ -137,7 +168,11 @@ function increaseState(idx: number, state: number[], steps: UnitStep[]): number[ } function decreaseState(idx: number, state: number[], steps: UnitStep[]): number[] { - if (idx === 0 && state[idx] === 0) { + if (isNaN(state[idx])) { + const newState = [...state] + newState[idx] = 0 + return newState + } else if (idx === 0 && state[0] === 0) { return state } else if (state[idx] === 0) { const newState = [...state] From 51814b29914a2881ac760bc6f88fa95990068486 Mon Sep 17 00:00:00 2001 From: Vincenzo Pierro Date: Tue, 25 Feb 2025 20:41:36 +0100 Subject: [PATCH 4/4] :lipstick: Added indicator for materials with no units left --- src/components/models/MaterialCard.tsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/models/MaterialCard.tsx b/src/components/models/MaterialCard.tsx index fdfb61b..ba0d28f 100644 --- a/src/components/models/MaterialCard.tsx +++ b/src/components/models/MaterialCard.tsx @@ -5,11 +5,13 @@ import { CardBody, CardFooter, CardHeader, + Container, Flex, Heading, Icon, IconButton, Text, + Tooltip, useDisclosure, } from '@chakra-ui/react' import { ElementTag } from './ElementTag' @@ -20,7 +22,7 @@ import { ConfirmModal } from '../modals/ConfirmModal' import { useIsMobileLayout } from '../../hooks/responsive-size' import { DetailedMaterialModal } from './DetailedMaterialModal' import { AddBoxFormModal } from '../modals/AddBoxFormModal' -import { PencilSimple, Plus, Trash } from '@phosphor-icons/react' +import { ExclamationMark, PencilSimple, Plus, Trash } from '@phosphor-icons/react' import { useHasPermission } from '../../hooks/permissions' import { Permissions } from '../../models/security/Permissions' import { useDeleteBoxesWithMaterialMutation, useGetUnitsWithMaterialQuery } from '../../services/box' @@ -85,8 +87,26 @@ export const MaterialCard = ({ material, isCompact }: MaterialCardProps) => { onClick={!isMobile ? openDetails : undefined} > {!!material.description && {material.description}} - {!!boxDefinition && !!totalInBoxes && ( - + {!!boxDefinition && totalInBoxes != null && ( + + + {totalInBoxes === 0 && ( + + + + + + )} + )} {!!material.tags && material.tags.length > 0 && (