Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/forms/UpdateQuantityForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -95,6 +99,7 @@ export const UpdateQuantityForm = ({ box, boxDefinition, onDispatched }: UpdateQ
/>
<BoxUnitSelector
boxUnit={boxDefinition}
boxCount={boxCount}
label="Select the quantity to update"
validator={input => !!input && !input.boxUnit && input.quantity > 0}
valueConsumer={value => {
Expand Down
52 changes: 48 additions & 4 deletions src/components/forms/controls/BoxUnitSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoxUnit>) => void
Expand All @@ -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
}
}, [boxCount, boxUnit])
const [stepValues, setStepValues] = useState<number[]>(
unitSteps
.slice(0, unitSteps.length - 1)
Expand Down Expand Up @@ -74,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 => {
Expand All @@ -94,7 +114,14 @@ export const BoxUnitSelector = ({
</FormLabel>
<HStack>
<Button onClick={() => onDecrease(idx)}>-</Button>
<Input width="5em" type="number" value={stepValues[idx] ?? 0} readOnly />
<Input
width="5em"
type="number"
value={stepValues[idx] ?? 0}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
onSetValue(idx, parseInt(event.currentTarget.value))
}}
/>
<Button onClick={() => onIncrease(idx)}>+</Button>
</HStack>
</FormControl>
Expand All @@ -111,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
Expand All @@ -128,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]
Expand Down
26 changes: 23 additions & 3 deletions src/components/models/MaterialCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
CardBody,
CardFooter,
CardHeader,
Container,
Flex,
Heading,
Icon,
IconButton,
Text,
Tooltip,
useDisclosure,
} from '@chakra-ui/react'
import { ElementTag } from './ElementTag'
Expand All @@ -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'
Expand Down Expand Up @@ -85,8 +87,26 @@ export const MaterialCard = ({ material, isCompact }: MaterialCardProps) => {
onClick={!isMobile ? openDetails : undefined}
>
{!!material.description && <Text>{material.description}</Text>}
{!!boxDefinition && !!totalInBoxes && (
<QuantityCounter quantity={totalInBoxes} boxDefinition={boxDefinition} mt="0.5em" />
{!!boxDefinition && totalInBoxes != null && (
<Flex>
<QuantityCounter quantity={totalInBoxes} boxDefinition={boxDefinition} mt="0.5em" />
{totalInBoxes === 0 && (
<Tooltip label="No units left for this material">
<Container
borderRadius="full"
backgroundColor="red"
width="1.7em"
height="1.7em"
paddingTop="0.15em"
paddingLeft="0.1em"
marginLeft="1em"
marginTop="0.5em"
>
<Icon as={ExclamationMark} boxSize={6} />
</Container>
</Tooltip>
)}
</Flex>
)}
{!!material.tags && material.tags.length > 0 && (
<Flex align="center" justify="start" mt="1em">
Expand Down
56 changes: 56 additions & 0 deletions src/pages/material/MaterialReportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Text>Download a report of the materials</Text>
</ModalHeader>
<ModalCloseButton />
<ModalBody pt="1em" pb="2em">
<Center>
<Button
leftIcon={<Icon as={FileArrowDown} boxSize={7} />}
colorScheme="green"
variant="solid"
size="lg"
onClick={handleDownload}
loadingText="Preparing report"
isLoading={isLoading}
>
Download Materials Report
</Button>
</Center>
</ModalBody>
</ModalContent>
</Modal>
)
}
19 changes: 17 additions & 2 deletions src/pages/material/SearchMaterialsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { setPageTitle } from '../../store/ui/ui-slice'
import {
Alert,
AlertIcon,
Button,
Center,
CloseButton,
Container,
Icon,
IconButton,
Input,
InputGroup,
InputLeftAddon,
Expand All @@ -15,6 +18,7 @@ import {
Skeleton,
Spinner,
Stack,
useDisclosure,
VStack,
} from '@chakra-ui/react'
import { useGetTagsQuery } from '../../services/tag'
Expand All @@ -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()
Expand All @@ -48,6 +55,7 @@ export const SearchMaterialsPage = () => {
const [rawQuery, setRawQuery] = useState<string>('')
const [query, setQuery] = useState<string | undefined>(undefined)
const [isTyping, setIsTyping] = useState<boolean>(false)
const { isOpen: reportModalIsOpen, onOpen: reportModalOpen, onClose: onReportModalClose } = useDisclosure()

const { data: tags, error: tagsError, isLoading: tagsLoading } = useGetTagsQuery()
const {
Expand All @@ -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) {
Expand Down Expand Up @@ -149,7 +156,7 @@ export const SearchMaterialsPage = () => {
<Input
id="material-search-bar"
placeholder="Search by name, brand, or reference number"
minWidth="75vw"
minWidth="65vw"
onChange={onChangeFilter}
/>
{isTyping && (
Expand All @@ -158,6 +165,13 @@ export const SearchMaterialsPage = () => {
</InputRightElement>
)}
</InputGroup>
<IconButton
colorScheme="green"
aria-label="Download excel report"
size="md"
icon={<Icon as={FileXls} boxSize={7} />}
onClick={reportModalOpen}
/>
</Stack>
{!!idsError && (
<ErrorAlert
Expand Down Expand Up @@ -202,6 +216,7 @@ export const SearchMaterialsPage = () => {
onNextEnter={prefetchNextPage}
/>
</Center>
<MaterialReportModal onClose={onReportModalClose} isOpen={reportModalIsOpen} />
</VStack>
)
}
12 changes: 10 additions & 2 deletions src/services/material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,26 @@ export const materialApi = createApi({
}),
providesTags: materialTagProvider,
}),
initReportCreation: builder.mutation<string, void>({
query: () => ({
url: '/report',
method: 'POST',
responseHandler: 'text',
}),
}),
}),
})

export const {
useCreateMaterialMutation,
useDeleteMaterialMutation,
useFilterMaterialsQuery,
useFindMaterialsByFuzzyNameQuery,
useGetLastCreatedQuery,
useGetMaterialsByIdsQuery,
useGetMaterialsByRefCodeQuery,
useGetMaterialQuery,
useFilterMaterialsQuery,
useFindMaterialsByFuzzyNameQuery,
useInitReportCreationMutation,
useModifyMaterialMutation,
useSearchIdsByNameBrandCodeQuery,
useSearchNamesByNameBrandCodeQuery,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/url-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getReportDownloadUrl(token: string): string {
return `${process.env.REACT_APP_APIURL}/material/report?token=${token}`
}