From 8f523d0a8f48b77f3422b0254adf51eb73900354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Geraldo=20D=2E=20F?= Date: Thu, 30 May 2024 18:43:12 -0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Carrinho=20de=20Doa=C3=A7=C3=B5es=20(#3?= =?UTF-8?q?40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +- src/App.tsx | 10 +- src/components/BackToTop/BackToTop.tsx | 62 ++-- src/components/BurgerMenu/BurgerMenu.tsx | 13 +- src/components/DonationCart/DonationCart.tsx | 45 +++ .../DonationCartForm/DonationCartForm.tsx | 273 +++++++++++++++ .../components/DonationCartForm/index.ts | 3 + .../components/DonationCartForm/types.ts | 5 + .../DonationSuccess/DonationSuccess.tsx | 83 +++++ .../components/DonationSuccess/index.ts | 3 + .../components/DonationSuccess/types.ts | 4 + .../DonationCart/components/index.ts | 4 + src/components/DonationCart/index.ts | 3 + src/components/DonationCart/types.ts | 5 + .../DonationCartIcon/DonationCartIcon.tsx | 29 ++ src/components/DonationCartIcon/index.ts | 3 + src/components/DonationCartIcon/types.ts | 3 + src/components/Header/Header.tsx | 12 +- src/components/SearchInput/SearchInput.tsx | 19 +- src/components/SearchInput/types.ts | 7 +- src/components/TextField/TextField.tsx | 2 +- src/components/index.ts | 6 + src/components/ui/sheet.tsx | 75 ++--- .../DonationCartContext.tsx | 99 ++++++ src/contexts/DonationCartContext/index.ts | 6 + src/contexts/DonationCartContext/types.ts | 25 ++ src/contexts/index.ts | 13 +- src/hooks/index.ts | 2 + src/hooks/useDonationOrder/index.ts | 3 + src/hooks/useDonationOrder/types.ts | 57 ++++ .../useDonationOrder/useDonationOrder.tsx | 8 + src/hooks/useFetch/useFetch.tsx | 3 +- src/hooks/usePaginatedQuery/paths.ts | 1 + .../usePaginatedQuery/usePaginatedQuery.tsx | 14 +- src/hooks/useShelter/types.ts | 9 + src/hooks/useShelters/types.ts | 3 +- src/hooks/useShelters/useShelters.tsx | 3 +- src/lib/utils.ts | 11 +- src/pages/Home/components/Filter/Filter.tsx | 4 +- .../ShelterListView/ShelterListView.tsx | 6 +- src/pages/Shelter/Shelter.tsx | 310 ++++++++++-------- .../ShelterCategoryItems.tsx | 112 ------- .../components/ShelterCategoryItems/index.ts | 3 - .../components/ShelterCategoryItems/types.ts | 14 - .../ShelterCategoryList.tsx | 57 ++++ .../components/ShelterCategoryList/index.ts | 3 + .../components/ShelterCategoryList/types.ts | 16 + src/pages/Shelter/components/index.ts | 4 +- .../donationOrder/donationOrder.service.ts | 34 ++ src/service/donationOrder/index.ts | 3 + src/service/donationOrder/types.ts | 51 +++ src/service/index.ts | 2 + .../shelterSupply/shelter-supply.service.ts | 26 +- src/service/shelterSupply/types.ts | 23 ++ src/service/users/types.ts | 4 + src/service/users/user.service.ts | 11 +- 56 files changed, 1227 insertions(+), 385 deletions(-) create mode 100644 src/components/DonationCart/DonationCart.tsx create mode 100644 src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx create mode 100644 src/components/DonationCart/components/DonationCartForm/index.ts create mode 100644 src/components/DonationCart/components/DonationCartForm/types.ts create mode 100644 src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx create mode 100644 src/components/DonationCart/components/DonationSuccess/index.ts create mode 100644 src/components/DonationCart/components/DonationSuccess/types.ts create mode 100644 src/components/DonationCart/components/index.ts create mode 100644 src/components/DonationCart/index.ts create mode 100644 src/components/DonationCart/types.ts create mode 100644 src/components/DonationCartIcon/DonationCartIcon.tsx create mode 100644 src/components/DonationCartIcon/index.ts create mode 100644 src/components/DonationCartIcon/types.ts create mode 100644 src/contexts/DonationCartContext/DonationCartContext.tsx create mode 100644 src/contexts/DonationCartContext/index.ts create mode 100644 src/contexts/DonationCartContext/types.ts create mode 100644 src/hooks/useDonationOrder/index.ts create mode 100644 src/hooks/useDonationOrder/types.ts create mode 100644 src/hooks/useDonationOrder/useDonationOrder.tsx delete mode 100644 src/pages/Shelter/components/ShelterCategoryItems/ShelterCategoryItems.tsx delete mode 100644 src/pages/Shelter/components/ShelterCategoryItems/index.ts delete mode 100644 src/pages/Shelter/components/ShelterCategoryItems/types.ts create mode 100644 src/pages/Shelter/components/ShelterCategoryList/ShelterCategoryList.tsx create mode 100644 src/pages/Shelter/components/ShelterCategoryList/index.ts create mode 100644 src/pages/Shelter/components/ShelterCategoryList/types.ts create mode 100644 src/service/donationOrder/donationOrder.service.ts create mode 100644 src/service/donationOrder/index.ts create mode 100644 src/service/donationOrder/types.ts diff --git a/.env.example b/.env.example index fa8ac5ee..ad8971f6 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ VITE_API_URL=http://localhost:4000 -VITE_HMAC_SECRET_KEY= \ No newline at end of file +VITE_HMAC_SECRET_KEY= +VITE_REQUEST_CACHE=false \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index f0dd1a25..af7fd636 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,9 +2,9 @@ import { Fragment } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { Routes } from './routes/Routes'; -import { SessionProvider } from './contexts'; +import { DonationCartProvider, SessionProvider } from './contexts'; import { Toaster } from './components/ui/toaster'; -import { BackToTop } from '@/components/BackToTop'; +import { BackToTop } from '@/components'; const App = () => { return ( @@ -12,8 +12,10 @@ const App = () => { - - + + + + diff --git a/src/components/BackToTop/BackToTop.tsx b/src/components/BackToTop/BackToTop.tsx index bc64c207..e1c418de 100644 --- a/src/components/BackToTop/BackToTop.tsx +++ b/src/components/BackToTop/BackToTop.tsx @@ -1,41 +1,45 @@ -import { useState } from "react" -import { ArrowUp } from "lucide-react" +import { useState } from 'react'; +import { ArrowUp } from 'lucide-react'; +const BackToTop = () => { + const [isVisible, setVisibility] = useState(false); -const BackToTop =() => { - - const [isVisible, setVisibility] = useState(false) - - const scrollToTop = () => { - let root = document.getElementById('root') - if (!root) {return} - - root.scrollTo({top:0, behavior:"smooth"}) - + const scrollToTop = () => { + const root = document.getElementById('root'); + if (!root) { + return; } - document.getElementById("root")?.addEventListener('scroll', (e) => { - if (e.target === null) {return} - let CurrentScrollHeight = (e.target as HTMLElement).scrollTop - let WindowHeight = window.innerHeight + root.scrollTo({ top: 0, behavior: 'smooth' }); + }; - if ( CurrentScrollHeight > WindowHeight / 2) { - setVisibility(true) - } else { - setVisibility(false) - } - }) + document.getElementById('root')?.addEventListener('scroll', (e) => { + if (e.target === null) { + return; + } + const CurrentScrollHeight = (e.target as HTMLElement).scrollTop; + const WindowHeight = window.innerHeight; + if (CurrentScrollHeight > WindowHeight / 2) { + setVisibility(true); + } else { + setVisibility(false); + } + }); -return (isVisible && ( - -)); -} + onClick={scrollToTop} + > + + + ) + ); +}; export { BackToTop }; diff --git a/src/components/BurgerMenu/BurgerMenu.tsx b/src/components/BurgerMenu/BurgerMenu.tsx index 2f823e5b..db04d613 100644 --- a/src/components/BurgerMenu/BurgerMenu.tsx +++ b/src/components/BurgerMenu/BurgerMenu.tsx @@ -8,6 +8,7 @@ import { LinkIcon, Menu, ShieldAlert, + X, } from 'lucide-react'; import { SessionServices } from '@/service'; @@ -16,6 +17,9 @@ import { BurguerMenuItem } from './components'; import { Separator } from '../ui/separator'; import { SessionContext } from '@/contexts'; import { usePartners } from '@/hooks'; +import { DialogClose } from '@radix-ui/react-dialog'; +import { Button } from '../ui/button'; +import { DialogFooter } from '../ui/dialog'; const BurgerMenu = () => { const { session } = useContext(SessionContext); @@ -37,7 +41,14 @@ const BurgerMenu = () => { - + + + + + +
{session && ( diff --git a/src/components/DonationCart/DonationCart.tsx b/src/components/DonationCart/DonationCart.tsx new file mode 100644 index 00000000..5fed55bd --- /dev/null +++ b/src/components/DonationCart/DonationCart.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; + +import { IDonationCart } from './types'; +import { Sheet, SheetContent } from '../ui/sheet'; +import { DonationCartForm, DonationSuccess } from './components'; + +const DonationCart = (props: IDonationCart) => { + const { onClose, opened, shelterId } = props; + const [donationOrderId, setDonationOrderId] = useState(null); + + useEffect(() => { + const el = document.querySelector('header'); + if (el) { + if (opened) { + el?.classList.remove('z-[100]'); + el?.classList.add('z-0'); + } else { + el?.classList.remove('z-0'); + el?.classList.add('z-[100]'); + } + } + }, [opened]); + + useEffect(() => { + if (!opened) setDonationOrderId(null); + }, [opened]); + + return ( + + + {donationOrderId ? ( + + ) : ( + setDonationOrderId(orderId)} + /> + )} + + + ); +}; + +export { DonationCart }; diff --git a/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx b/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx new file mode 100644 index 00000000..98b90f80 --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx @@ -0,0 +1,273 @@ +import React, { useCallback, useContext, useMemo, useState } from 'react'; +import { Trash2 } from 'lucide-react'; + +import { + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '../../../ui/sheet'; +import { DonationCartContext, SessionContext } from '@/contexts'; +import { Input } from '../../../ui/input'; +import { Button } from '../../../ui/button'; +import { SupplyMeasureMap, cn, getSupplyPriorityProps } from '@/lib/utils'; +import { CircleStatus } from '../../../CircleStatus'; +import { Separator } from '../../../ui/separator'; +import { IDonationCartItem } from '@/contexts/DonationCartContext/types'; +import { + DonationOrderServices, + SessionServices, + // ShelterSupplyServices, + UserServices, +} from '@/service'; +import { IDonateItem } from '@/service/donationOrder/types'; +import { TextField } from '../../../TextField'; +import { ICreateUser } from '@/service/users/types'; +import { IDonationCartForm } from './types'; + +const DonationCartForm = React.forwardRef( + (props, ref) => { + const { shelterId, onCancel, onSuccess, className = '', ...rest } = props; + const { refreshSession, session } = useContext(SessionContext); + const { carts, removeItem, clearCart, updateItem } = + useContext(DonationCartContext); + const [loading, setLoading] = useState(false); + const cart = useMemo(() => carts[shelterId] ?? [], [carts, shelterId]); + const [errors, setErrors] = useState>({}); + const [values, setValues] = useState>({}); + + const handleCancelCart = useCallback(() => { + clearCart(shelterId); + if (onCancel) onCancel(); + }, [clearCart, onCancel, shelterId]); + + const handleChangeQuantity = useCallback( + (item: IDonationCartItem, quantity: number) => { + setValues((prev) => ({ + ...prev, + [item.id]: quantity, + })); + updateItem(shelterId, item.id, { quantity }); + }, + [shelterId, updateItem] + ); + + // const verifyCartItems = useCallback( + // async ( + // shelterId: string, + // items: IDonateItem[] + // ): Promise> => { + // const { data } = await ShelterSupplyServices.getAll(shelterId); + // const newErrors = items.reduce((prev, current) => { + // const finded = data.find((d) => d.supply.id === current.id); + // const ok = current.quantity <= (finded?.quantity ?? 0); + // if (ok || !finded) return prev; + // else { + // const measure = SupplyMeasureMap[finded.supply.measure]; + // return { + // ...prev, + // [current.id]: `A doação de ${finded.supply.name} não pode ser maior que a quantidade máxima de ${finded.quantity}${measure} `, + // }; + // } + // }, {}); + // return newErrors; + // }, + // [] + // ); + + const verifyAccountExists = useCallback(async (phone: string) => { + const { data } = await UserServices.find('phone', phone); + if (data.exists) { + setErrors({ + phone: + 'Já existe um usuário com este telefone. Faça login ou tente outro telefone', + }); + return false; + } + return true; + }, []); + + const handleCreateAccount = useCallback( + async (payload: Record) => { + const { lastName, name, phone } = payload; + if (name && lastName && phone) { + const ok = await verifyAccountExists(phone.toString()); + if (!ok) return false; + + await UserServices.create({ + phone: phone.toString(), + name: name.toString(), + lastName: lastName.toString(), + }); + const parsedPhone = phone.toString().replace(/[^0-9]/g, ''); + const resp = await SessionServices.auth({ + login: parsedPhone, + password: parsedPhone, + }); + localStorage.setItem('token', resp.token); + refreshSession(); + } + + return true; + }, + [refreshSession, verifyAccountExists] + ); + + const handleDonate = useCallback( + async (ev: React.FormEvent) => { + ev.preventDefault(); + + setLoading(true); + try { + const form = new FormData(ev.currentTarget); + const formData = Object.fromEntries(form); + const { name, lastName, phone, ...rest } = formData; + const ok = await handleCreateAccount({ name, lastName, phone }); + if (ok) { + const items = Object.entries(rest).reduce( + (prev, [key, value]) => [...prev, { id: key, quantity: +value }], + [] as IDonateItem[] + ); + //TODO: discutir produto se vai e como será verificado os "erros" do carrinho + // const errorsData = await verifyCartItems(shelterId, items); + // setErrors(errorsData); + // if (Object.keys(errorsData).length === 0) { + const resp = await DonationOrderServices.store({ + shelterId, + supplies: items, + }); + if (onSuccess) onSuccess(resp.data.id); + clearCart(shelterId); + // } + } + } catch (err) { + console.log('Ocorreu um erro ao realizar a doação'); + } finally { + setLoading(false); + } + }, + [clearCart, handleCreateAccount, onSuccess, shelterId] + ); + + return ( +
+ + + {[session?.name, 'Revise sua doação'].filter((p) => !!p).join(', ')} + + + Ajuste a quantidade que gostaria de doar em cada item + + +
+
+ Item + Quantidade +
+ {cart.map((item) => { + const { className } = getSupplyPriorityProps(item.priority); + return ( +
+
+
+ + {item.name} +
+
+
+ + handleChangeQuantity(item, +ev.target.value) + } + /> + + {SupplyMeasureMap[item.measure]} + +
+ +
+
+ {errors[item.id] && ( + +

{errors[item.id]}

+
+ )} +
+ ); + })} + + {!session && ( +
+

Comunique sua doação

+

+ Utilizaremos seus dados apenas para comunicar ao abrigo que sua + doação esta a caminho. +

+
+ + + +
+
+ )} +
+ +
+ + +
+
+
+ ); + } +); + +export { DonationCartForm }; diff --git a/src/components/DonationCart/components/DonationCartForm/index.ts b/src/components/DonationCart/components/DonationCartForm/index.ts new file mode 100644 index 00000000..f07b238a --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/index.ts @@ -0,0 +1,3 @@ +import { DonationCartForm } from './DonationCartForm'; + +export { DonationCartForm }; diff --git a/src/components/DonationCart/components/DonationCartForm/types.ts b/src/components/DonationCart/components/DonationCartForm/types.ts new file mode 100644 index 00000000..85a28027 --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/types.ts @@ -0,0 +1,5 @@ +export interface IDonationCartForm extends React.ComponentPropsWithRef<'form'> { + shelterId: string; + onCancel?: () => void; + onSuccess?: (donationOrderId: string) => void; +} diff --git a/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx new file mode 100644 index 00000000..6418d37e --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Circle, HeartHandshake, Loader, Printer } from 'lucide-react'; + +import { + SheetHeader, + SheetTitle, + SheetDescription, +} from '@/components/ui/sheet'; +import { IDonationSuccessProps } from './types'; +import { SupplyMeasureMap, cn } from '@/lib/utils'; +import { useDonationOrder } from '@/hooks'; +import { format } from 'date-fns'; +import { Button } from '@/components/ui/button'; + +const DonationSuccess = React.forwardRef( + (props, ref) => { + const { donationOrderId, className = '', ...rest } = props; + const { data: donation, loading } = useDonationOrder(donationOrderId); + + if (loading) + return ; + + return ( +
+ + O RS agradece sua doação + + + + +
+
+ + Cada doação importa. + + + Juntos somos mais fortes! + +
+
+
+ + Doação para + +

{donation.shelter.name}

+ + às{' '} + {format(new Date(donation.createdAt), "HH'h'mm 'de' dd/MM/yy")} + +
+
    + {donation.donationOrderSupplies.map((s, idx) => ( +
  • + + {s.quantity} + {SupplyMeasureMap[s.supply.measure]} de {s.supply.name} +
  • + ))} +
+ +
+
+
+ +
+
+ ); + } +); + +export { DonationSuccess }; diff --git a/src/components/DonationCart/components/DonationSuccess/index.ts b/src/components/DonationCart/components/DonationSuccess/index.ts new file mode 100644 index 00000000..aef6420e --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/index.ts @@ -0,0 +1,3 @@ +import { DonationSuccess } from './DonationSuccess'; + +export { DonationSuccess }; diff --git a/src/components/DonationCart/components/DonationSuccess/types.ts b/src/components/DonationCart/components/DonationSuccess/types.ts new file mode 100644 index 00000000..12de8593 --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/types.ts @@ -0,0 +1,4 @@ +export interface IDonationSuccessProps + extends React.ComponentPropsWithoutRef<'div'> { + donationOrderId: string; +} diff --git a/src/components/DonationCart/components/index.ts b/src/components/DonationCart/components/index.ts new file mode 100644 index 00000000..ffced286 --- /dev/null +++ b/src/components/DonationCart/components/index.ts @@ -0,0 +1,4 @@ +import { DonationCartForm } from './DonationCartForm'; +import { DonationSuccess } from './DonationSuccess'; + +export { DonationCartForm, DonationSuccess }; diff --git a/src/components/DonationCart/index.ts b/src/components/DonationCart/index.ts new file mode 100644 index 00000000..0013386e --- /dev/null +++ b/src/components/DonationCart/index.ts @@ -0,0 +1,3 @@ +import { DonationCart } from './DonationCart'; + +export { DonationCart }; diff --git a/src/components/DonationCart/types.ts b/src/components/DonationCart/types.ts new file mode 100644 index 00000000..bdd8708b --- /dev/null +++ b/src/components/DonationCart/types.ts @@ -0,0 +1,5 @@ +export interface IDonationCart { + shelterId: string; + opened: boolean; + onClose: () => void; +} diff --git a/src/components/DonationCartIcon/DonationCartIcon.tsx b/src/components/DonationCartIcon/DonationCartIcon.tsx new file mode 100644 index 00000000..478e7959 --- /dev/null +++ b/src/components/DonationCartIcon/DonationCartIcon.tsx @@ -0,0 +1,29 @@ +import { useContext } from 'react'; +import { HandHeart } from 'lucide-react'; + +import { Button } from '../ui/button'; +import { DonationCartContext } from '@/contexts'; +import { IDonationCartIconProps } from './types'; + +const DonationCartIcon = (props: IDonationCartIconProps) => { + const { quantity } = props; + const { toggleOpened } = useContext(DonationCartContext); + + return ( + + ); +}; + +export { DonationCartIcon }; diff --git a/src/components/DonationCartIcon/index.ts b/src/components/DonationCartIcon/index.ts new file mode 100644 index 00000000..7c7ffa6b --- /dev/null +++ b/src/components/DonationCartIcon/index.ts @@ -0,0 +1,3 @@ +import { DonationCartIcon } from './DonationCartIcon'; + +export { DonationCartIcon }; diff --git a/src/components/DonationCartIcon/types.ts b/src/components/DonationCartIcon/types.ts new file mode 100644 index 00000000..eee4304e --- /dev/null +++ b/src/components/DonationCartIcon/types.ts @@ -0,0 +1,3 @@ +export interface IDonationCartIconProps { + quantity?: number; +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 00e1529e..b3255f6d 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { IHeader } from './types'; import { cn } from '@/lib/utils'; -import { Link } from 'react-router-dom'; const Header = React.forwardRef((props, ref) => { const { @@ -14,10 +14,10 @@ const Header = React.forwardRef((props, ref) => { } = props; return ( -
((props, ref) => { {title}
-
-
{endAdornment}
-
-
+
{endAdornment}
+ ); }); diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 225ddb87..cbd723d2 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -7,21 +7,26 @@ import { cn } from '@/lib/utils'; const SearchInput = React.forwardRef( (props, ref) => { + const { inputProps, value, onChange, className, ...rest } = props; const { - value, - onChange, - className, placeholder = 'Buscar por abrigo ou endereço', - ...rest - } = props; + className: inputClassName = '', + ...restInputProps + } = inputProps ?? {}; return (
+ onChange ? onChange(ev.target.value ?? '') : undefined + } + {...restInputProps} />
diff --git a/src/components/SearchInput/types.ts b/src/components/SearchInput/types.ts index b15517df..9594490e 100644 --- a/src/components/SearchInput/types.ts +++ b/src/components/SearchInput/types.ts @@ -1,4 +1,9 @@ export interface ISearchInputProps - extends React.ComponentPropsWithoutRef<'input'> { + extends Omit, 'onChange'> { value: string; + onChange?: (value: string) => void; + inputProps?: Omit< + React.ComponentPropsWithoutRef<'input'>, + 'value' | 'onChange' + >; } diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index db4fa822..43a2adf0 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -14,7 +14,7 @@ const TextField = forwardRef, TextFieldProps>( className, error, helperText, - value = '', + value, ...rest } = props; diff --git a/src/components/index.ts b/src/components/index.ts index 542ce455..fe877999 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,6 +14,9 @@ import { VerifiedBadge } from './VerifiedBadge'; import { SelectField } from './SelectField'; import { Authenticated } from './Authenticated'; import { BurgerMenu } from './BurgerMenu'; +import { BackToTop } from './BackToTop'; +import { DonationCartIcon } from './DonationCartIcon'; +import { DonationCart } from './DonationCart'; export { LoadingScreen, @@ -32,4 +35,7 @@ export { SelectField, Authenticated, BurgerMenu, + BackToTop, + DonationCartIcon, + DonationCart, }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 117f9957..d64a6fa7 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,17 +1,17 @@ -import * as React from 'react'; -import * as SheetPrimitive from '@radix-ui/react-dialog'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" -import { cn } from '@/lib/utils'; -import { X } from 'lucide-react'; +import { cn } from "@/lib/utils" -const Sheet = SheetPrimitive.Root; +const Sheet = SheetPrimitive.Root -const SheetTrigger = SheetPrimitive.Trigger; +const SheetTrigger = SheetPrimitive.Trigger -const SheetClose = SheetPrimitive.Close; +const SheetClose = SheetPrimitive.Close -const SheetPortal = SheetPrimitive.Portal; +const SheetPortal = SheetPrimitive.Portal const SheetOverlay = React.forwardRef< React.ElementRef, @@ -19,33 +19,33 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName const sheetVariants = cva( - 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", { variants: { side: { - top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", bottom: - 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', - left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", right: - 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", }, }, defaultVariants: { - side: 'right', + side: "right", }, } -); +) interface SheetContentProps extends React.ComponentPropsWithoutRef, @@ -54,7 +54,7 @@ interface SheetContentProps const SheetContent = React.forwardRef< React.ElementRef, SheetContentProps ->(({ side = 'right', className, children, ...props }, ref) => ( +>(({ side = "right", className, children, ...props }, ref) => ( {children} - - + + + Close -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; +)) +SheetContent.displayName = SheetPrimitive.Content.displayName const SheetHeader = ({ className, @@ -77,13 +78,13 @@ const SheetHeader = ({ }: React.HTMLAttributes) => (
-); -SheetHeader.displayName = 'SheetHeader'; +) +SheetHeader.displayName = "SheetHeader" const SheetFooter = ({ className, @@ -91,13 +92,13 @@ const SheetFooter = ({ }: React.HTMLAttributes) => (
-); -SheetFooter.displayName = 'SheetFooter'; +) +SheetFooter.displayName = "SheetFooter" const SheetTitle = React.forwardRef< React.ElementRef, @@ -105,11 +106,11 @@ const SheetTitle = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName const SheetDescription = React.forwardRef< React.ElementRef, @@ -117,11 +118,11 @@ const SheetDescription = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName export { Sheet, @@ -134,4 +135,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -}; +} diff --git a/src/contexts/DonationCartContext/DonationCartContext.tsx b/src/contexts/DonationCartContext/DonationCartContext.tsx new file mode 100644 index 00000000..138bf9f6 --- /dev/null +++ b/src/contexts/DonationCartContext/DonationCartContext.tsx @@ -0,0 +1,99 @@ +import React, { createContext, useCallback, useEffect, useState } from 'react'; + +import { IDonationCartContext, IDonationCartItem } from './types'; + +function getDonationCart(): Record { + const data = localStorage.getItem('shelter-carts'); + if (!data) return {}; + return JSON.parse(data); +} + +const DonationCartContext = createContext({} as IDonationCartContext); + +const DonationCartProvider = ({ children }: { children?: React.ReactNode }) => { + const [opened, setOpened] = useState(false); + const [carts, setCarts] = useState>( + getDonationCart() + ); + + const addItem = useCallback((shelterId: string, item: IDonationCartItem) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + if (prev.some((p) => p.id === item.id)) + return { + ...state, + [shelterId]: prev.map((p) => + p.id === item.id + ? { ...p, quantity: p.quantity + item.quantity } + : p + ), + }; + else return { ...state, [shelterId]: [...prev, item] }; + }); + }, []); + + const updateItem = useCallback( + ( + shelterId: string, + supplyId: string, + payload: Partial> + ) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + if (prev.some((p) => p.id === supplyId)) + return { + ...state, + [shelterId]: prev.map((p) => + p.id === supplyId ? { ...p, ...payload } : p + ), + }; + else return state; + }); + }, + [] + ); + + const removeItem = useCallback((shelterId: string, supplyId: string) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + return { ...state, [shelterId]: prev.filter((p) => p.id !== supplyId) }; + }); + }, []); + + const toggleOpened = useCallback(() => setOpened((prev) => !prev), []); + + const clearCart = useCallback( + (shelterId: string) => setCarts((state) => ({ ...state, [shelterId]: [] })), + [] + ); + + const updateCart = useCallback( + (shelterId: string, items: IDonationCartItem[]) => { + setCarts((prev) => ({ ...prev, [shelterId]: items })); + }, + [] + ); + + useEffect(() => { + localStorage.setItem('shelter-carts', JSON.stringify(carts)); + }, [carts]); + + return ( + + {children} + + ); +}; + +export { DonationCartContext, DonationCartProvider }; diff --git a/src/contexts/DonationCartContext/index.ts b/src/contexts/DonationCartContext/index.ts new file mode 100644 index 00000000..a66d1068 --- /dev/null +++ b/src/contexts/DonationCartContext/index.ts @@ -0,0 +1,6 @@ +import { + DonationCartContext, + DonationCartProvider, +} from './DonationCartContext'; + +export { DonationCartContext, DonationCartProvider }; diff --git a/src/contexts/DonationCartContext/types.ts b/src/contexts/DonationCartContext/types.ts new file mode 100644 index 00000000..14539cb7 --- /dev/null +++ b/src/contexts/DonationCartContext/types.ts @@ -0,0 +1,25 @@ +import { SupplyMeasure } from '@/hooks/useShelter/types'; +import { SupplyPriority } from '@/service/supply/types'; + +export interface IDonationCartItem { + id: string; + name: string; + quantity: number; + priority: SupplyPriority; + measure: SupplyMeasure; +} + +export interface IDonationCartContext { + carts: Record; + opened: boolean; + toggleOpened: () => void; + addItem: (shelterId: string, item: IDonationCartItem) => void; + removeItem: (shelterId: string, supplyId: string) => void; + updateItem: ( + shelterId: string, + supplyId: string, + payload: Partial> + ) => void; + clearCart: (shelterId: string) => void; + updateCart: (shelterId: string, items: IDonationCartItem[]) => void; +} diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 2d3ea5bd..7eeef33b 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,3 +1,12 @@ -import { SessionContext, SessionProvider } from "./SessionContext"; +import { SessionContext, SessionProvider } from './SessionContext'; +import { + DonationCartContext, + DonationCartProvider, +} from './DonationCartContext'; -export { SessionContext, SessionProvider }; +export { + SessionContext, + SessionProvider, + DonationCartContext, + DonationCartProvider, +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a8007959..f28c05ed 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -12,6 +12,7 @@ import { usePartners } from './usePartners'; import { useGithubContributors } from './useGithubContributors'; import { useAuthRoles } from './useAuthRoles'; import { useSupporters } from './useSupporters'; +import { useDonationOrder } from './useDonationOrder'; export { useShelters, @@ -28,4 +29,5 @@ export { useGithubContributors, useAuthRoles, useSupporters, + useDonationOrder, }; diff --git a/src/hooks/useDonationOrder/index.ts b/src/hooks/useDonationOrder/index.ts new file mode 100644 index 00000000..f0211436 --- /dev/null +++ b/src/hooks/useDonationOrder/index.ts @@ -0,0 +1,3 @@ +import { useDonationOrder } from './useDonationOrder'; + +export { useDonationOrder }; diff --git a/src/hooks/useDonationOrder/types.ts b/src/hooks/useDonationOrder/types.ts new file mode 100644 index 00000000..e15144de --- /dev/null +++ b/src/hooks/useDonationOrder/types.ts @@ -0,0 +1,57 @@ +export enum ShelterCategory { + Shelter = 'Shelter', + DistributionCenter = 'DistributionCenter', +} + +export enum SupplyMeasure { + Unit = 'Unit', + Kg = 'Kg', + Litters = 'Litters', + Box = 'Box', + Piece = 'Piece', +} + +export interface IUseShelterData { + id: string; + name: string; + street?: string; + neighbourhood?: string; + city?: string; + streetNumber?: string | null; + zipCode?: string; + address: string; + pix?: string | null; + shelteredPeople?: number | null; + capacity?: number | null; + contact?: string | null; + petFriendly?: boolean | null; + prioritySum: number; + verified: boolean; + latitude?: string | null; + longitude?: string | null; + shelterSupplies: IUseShelterDataSupply[]; + category: ShelterCategory; + actived: boolean; + createdAt: string; + updatedAt?: string | null; +} + +export interface IUseShelterDataSupply { + priority: number; + quantity?: number | null; + supply: IUseShelterDataSupplyData; +} + +export interface IUseShelterDataSupplyData { + id: string; + name: string; + measure: SupplyMeasure; + supplyCategory: IUseShelterDataSupplyCategory; + createdAt: string; + updatedAt?: string | null; +} + +export interface IUseShelterDataSupplyCategory { + id: string; + name: string; +} diff --git a/src/hooks/useDonationOrder/useDonationOrder.tsx b/src/hooks/useDonationOrder/useDonationOrder.tsx new file mode 100644 index 00000000..f80513fe --- /dev/null +++ b/src/hooks/useDonationOrder/useDonationOrder.tsx @@ -0,0 +1,8 @@ +import { IDonateOrderItem } from '@/service/donationOrder/types'; +import { useFetch } from '../useFetch'; + +const useDonationOrder = (donationOrderId: string) => { + return useFetch(`/donation/order/${donationOrderId}`); +}; + +export { useDonationOrder }; diff --git a/src/hooks/useFetch/useFetch.tsx b/src/hooks/useFetch/useFetch.tsx index 121f5795..887833bd 100644 --- a/src/hooks/useFetch/useFetch.tsx +++ b/src/hooks/useFetch/useFetch.tsx @@ -13,7 +13,8 @@ function useFetch(path?: string, options: IUseFetchOptions = {}) { const refresh = useCallback( (config?: AxiosRequestConfig) => { const headers = config?.headers ?? {}; - if (cache) headers['x-app-cache'] = 'true'; + if (cache && import.meta.env.VITE_REQUEST_CACHE !== 'false') + headers['x-app-cache'] = 'true'; setLoading(true); if (path) { diff --git a/src/hooks/usePaginatedQuery/paths.ts b/src/hooks/usePaginatedQuery/paths.ts index 5c1d2cc5..6dde3171 100644 --- a/src/hooks/usePaginatedQuery/paths.ts +++ b/src/hooks/usePaginatedQuery/paths.ts @@ -3,4 +3,5 @@ export enum PaginatedQueryPath { ShelterCities = '/shelters/cities', SupplyCategories = '/supply-categories', Supplies = '/supplies', + DonationOrder = '/donation/order', } diff --git a/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx b/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx index 11009abd..4229807e 100644 --- a/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx +++ b/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx @@ -6,7 +6,10 @@ import { PaginatedQueryPath } from './paths'; import { IPaginatedResponse } from './types'; import { IServerResponse } from '@/types'; -function usePaginatedQuery(path: string | PaginatedQueryPath) { +function usePaginatedQuery( + path: string | PaginatedQueryPath, + defaultConfig: AxiosRequestConfig = {} +) { const [loading, setLoading] = useState(false); const [data, setData] = useState>({ count: 0, @@ -16,14 +19,17 @@ function usePaginatedQuery(path: string | PaginatedQueryPath) { }); const refresh = useCallback( - (config?: AxiosRequestConfig) => { + (config: AxiosRequestConfig = {}) => { setLoading(true); api - .get>>(path, config) + .get>>(path, { + ...defaultConfig, + ...config, + }) .then(({ data }) => setData(data.data)) .finally(() => setLoading(false)); }, - [path] + [defaultConfig, path] ); useEffect(() => { diff --git a/src/hooks/useShelter/types.ts b/src/hooks/useShelter/types.ts index 22c3bd67..e15144de 100644 --- a/src/hooks/useShelter/types.ts +++ b/src/hooks/useShelter/types.ts @@ -3,6 +3,14 @@ export enum ShelterCategory { DistributionCenter = 'DistributionCenter', } +export enum SupplyMeasure { + Unit = 'Unit', + Kg = 'Kg', + Litters = 'Litters', + Box = 'Box', + Piece = 'Piece', +} + export interface IUseShelterData { id: string; name: string; @@ -37,6 +45,7 @@ export interface IUseShelterDataSupply { export interface IUseShelterDataSupplyData { id: string; name: string; + measure: SupplyMeasure; supplyCategory: IUseShelterDataSupplyCategory; createdAt: string; updatedAt?: string | null; diff --git a/src/hooks/useShelters/types.ts b/src/hooks/useShelters/types.ts index 057bfcbe..4ad161e6 100644 --- a/src/hooks/useShelters/types.ts +++ b/src/hooks/useShelters/types.ts @@ -1,5 +1,5 @@ import { ShelterTagType } from '@/pages/Home/components/ShelterListItem/types'; -import { ShelterCategory } from '../useShelter/types'; +import { ShelterCategory, SupplyMeasure } from '../useShelter/types'; export interface IUseSheltersData { id: string; @@ -29,6 +29,7 @@ export interface IUseSheltersData { export interface IUseSheltersDataSupplyData { supply: { name: string; + measure: SupplyMeasure; supplyCategory: { name: string }; }; priority: number; diff --git a/src/hooks/useShelters/useShelters.tsx b/src/hooks/useShelters/useShelters.tsx index f6959800..13e51ac4 100644 --- a/src/hooks/useShelters/useShelters.tsx +++ b/src/hooks/useShelters/useShelters.tsx @@ -20,7 +20,8 @@ const useShelters = (options: IUseShelterOptions = {}) => { (config: AxiosRequestConfig = {}, append: boolean = false) => { const { search, ...rest } = (config ?? {}).params ?? {}; const headers = config.headers ?? {}; - if (cache) headers['x-app-cache'] = 'true'; + if (cache && import.meta.env.VITE_REQUEST_CACHE !== 'false') + headers['x-app-cache'] = 'true'; if (!append) setLoading(true); api .get>('/shelters', { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c12c1b59..42513669 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { ShelterCategory } from '@/hooks/useShelter/types'; +import { ShelterCategory, SupplyMeasure } from '@/hooks/useShelter/types'; import { IUseSheltersDataSupplyData } from '@/hooks/useShelters/types'; import { ShelterTagInfo, @@ -150,6 +150,14 @@ function checkIsNull(v?: any | null) { return v !== null && v !== undefined; } +const SupplyMeasureMap: Record = { + Box: 'caixa(s)', + Kg: 'kg', + Litters: 'litro(s)', + Piece: 'peça(s)', + Unit: 'un', +}; + export { cn, getAvailabilityProps, @@ -160,4 +168,5 @@ export { removeDuplicatesByField, normalizedCompare, checkIsNull, + SupplyMeasureMap, }; diff --git a/src/pages/Home/components/Filter/Filter.tsx b/src/pages/Home/components/Filter/Filter.tsx index d041a5d4..d19ebf1b 100644 --- a/src/pages/Home/components/Filter/Filter.tsx +++ b/src/pages/Home/components/Filter/Filter.tsx @@ -157,9 +157,7 @@ const Filter = (props: IFilterProps) => {
- setFieldValue('search', ev.target.value ?? '') - } + onChange={(v) => setFieldValue('search', v)} />
diff --git a/src/pages/Home/components/ShelterListView/ShelterListView.tsx b/src/pages/Home/components/ShelterListView/ShelterListView.tsx index ec57fc7b..332998b4 100644 --- a/src/pages/Home/components/ShelterListView/ShelterListView.tsx +++ b/src/pages/Home/components/ShelterListView/ShelterListView.tsx @@ -46,10 +46,8 @@ const ShelterListView = React.forwardRef( /> - onSearchValueChange - ? onSearchValueChange(ev.target.value ?? '') - : undefined + onChange={(value) => + onSearchValueChange ? onSearchValueChange(value) : undefined } />
diff --git a/src/pages/Shelter/Shelter.tsx b/src/pages/Shelter/Shelter.tsx index 78fea039..7b41487c 100644 --- a/src/pages/Shelter/Shelter.tsx +++ b/src/pages/Shelter/Shelter.tsx @@ -1,51 +1,51 @@ -import { useCallback, useMemo, useState } from 'react'; +import { Fragment, useCallback, useContext, useMemo, useState } from 'react'; import { ChevronLeft, Pencil } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; -import { format } from 'date-fns'; import { Authenticated, CardAboutShelter, + Chip, + DonationCart, + DonationCartIcon, Header, LoadingScreen, + SearchInput, } from '@/components'; import { useShelter } from '@/hooks'; -import { IShelterAvailabilityProps } from '@/pages/Home/components/ShelterListItem/types'; -import { cn, getAvailabilityProps, group } from '@/lib/utils'; +import { + cn, + getAvailabilityProps, + getSupplyPriorityProps, + group, + normalizedCompare, +} from '@/lib/utils'; import { Button } from '@/components/ui/button'; -import { ShelterCategoryItems } from './components'; +import { VerifiedBadge } from '@/components/VerifiedBadge/VerifiedBadge.tsx'; import { - IShelterCategoryItemsProps, - ITagItem, -} from './components/ShelterCategoryItems/types'; + IUseShelterDataSupply, + ShelterCategory, +} from '@/hooks/useShelter/types'; +import { IShelterAvailabilityProps } from '../Home/components/ShelterListItem/types'; import { SupplyPriority } from '@/service/supply/types'; -import { VerifiedBadge } from '@/components/VerifiedBadge/VerifiedBadge.tsx'; -import { ShelterSupplyServices } from '@/service'; -import { useToast } from '@/components/ui/use-toast'; -import { clearCache } from '@/api/cache'; -import { ShelterCategory } from '@/hooks/useShelter/types'; +import { ShelterCategoryList } from './components'; +import { Separator } from '@/components/ui/separator'; +import { DonationCartContext } from '@/contexts'; +import { ShelterCategoryListItemProps } from './components/ShelterCategoryList/types'; + +const defaultPriorities: SupplyPriority[] = [ + SupplyPriority.Urgent, + SupplyPriority.Needing, + SupplyPriority.Remaining, +]; const Shelter = () => { const params = useParams(); const { shelterId = '-1' } = params; const navigate = useNavigate(); - const { data: shelter, loading, refresh } = useShelter(shelterId); - const [selectedTags, setSelectedTags] = useState([]); - const shelterCategories: IShelterCategoryItemsProps[] = useMemo(() => { - const grouped = group(shelter?.shelterSupplies ?? [], 'priority'); - delete grouped[SupplyPriority.NotNeeded]; - - return Object.entries(grouped) - .sort(([a], [b]) => (+a > +b ? -1 : 1)) - .map(([key, values]) => ({ - priority: +key, - tags: values.map((v) => ({ - label: v.supply.name, - value: v.supply.id, - quantity: v.quantity, - })), - })); - }, [shelter?.shelterSupplies]); + const { toggleOpened, addItem, opened, carts } = + useContext(DonationCartContext); + const { data: shelter, loading } = useShelter(shelterId); const { availability, className: availabilityClassName } = useMemo( () => @@ -56,130 +56,170 @@ const Shelter = () => { }), [shelter?.capacity, shelter?.shelteredPeople, shelter?.category] ); - const [loadingUpdateMany, setLoadingUpdateMany] = useState(false); - const { toast } = useToast(); + const [priorities, setPriorities] = + useState(defaultPriorities); + const [search, setSearch] = useState(''); + + const supplyGroups = useMemo(() => { + if (!shelter?.shelterSupplies) return {}; + const groups = group(shelter.shelterSupplies, 'supply.supplyCategory.name'); + return Object.entries(groups).reduce((prev, [name, list]) => { + const filtered = list.filter( + (l) => + priorities.includes(l.priority) && + (!search || normalizedCompare(l.supply.name, search)) + ); + if (filtered.length > 0) return { [name]: filtered, ...prev }; + else return prev; + }, {} as Record); + }, [shelter, priorities, search]); - const handleSelectTag = useCallback((v: ITagItem) => { - setSelectedTags((prev) => - prev.includes(v) ? prev.filter((p) => p.value !== v.value) : [...prev, v] + const handleSelectPriority = (priority: SupplyPriority) => { + setPriorities((prev) => + prev.includes(priority) + ? prev.filter((p) => p !== priority) + : [...prev, priority] ); - }, []); + }; - const handleUpdateMany = useCallback(() => { - setLoadingUpdateMany(true); - ShelterSupplyServices.updateMany( - shelterId, - selectedTags.map((s) => s.value) - ) - .then(() => { - toast({ - title: 'Atualizado com sucesso', - }); - clearCache(false); - refresh(); - setSelectedTags([]); - }) - .catch((err) => { - toast({ - title: 'Erro ao atualizar', - description: `${err?.response?.data?.message ?? err?.message ?? err}`, - }); - }) - .finally(() => { - setLoadingUpdateMany(false); + const handleDonate = useCallback( + (item: ShelterCategoryListItemProps) => { + if (!opened) { + const hasViewedCart = + localStorage.getItem('has-viewed-cart') === 'true'; + if (!hasViewedCart) { + localStorage.setItem('has-viewed-cart', 'true'); + toggleOpened(); + } + } + addItem(shelterId, { + ...item, + quantity: item.quantity || 1, }); - }, [refresh, selectedTags, shelterId, toast]); + }, + [addItem, opened, shelterId, toggleOpened] + ); if (loading) return ; return ( -
-
navigate('/')} - > - - - } + + -
-
-

- {shelter.name} -

- {shelter.verified && } -
-
-

- {availability} -

- +
+
navigate(`/abrigo/${shelterId}/atualizar`)} + className="[&_svg]:stroke-white disabled:bg-red-500 hover:bg-red-400" + onClick={() => navigate('/')} > - Editar - + - -
-
- -
-
-

Itens do abrigo

-
- + +
-
-
- {shelterCategories.map((categoryProps, idx) => ( - + +
+
+

Itens do abrigo

+
+ +
+
+
+ setSearch(value)} + inputProps={{ + placeholder: 'Digite o item a doar', + }} /> - ))} -
- {shelter.updatedAt && ( -
- - Atualizado em {format(shelter.updatedAt, 'dd/MM/yyyy HH:mm')} -
- )} - -
- +
+ {defaultPriorities.map((priority, idx) => { + const { label, className } = getSupplyPriorityProps(priority); + return ( + handleSelectPriority(priority)} + /> + ); + })} +
+
+ {Object.entries(supplyGroups) + .sort((a, b) => (a[0] > b[0] ? 1 : -1)) + .map(([name, list], idx, arr) => { + const isLastElement = idx === arr.length - 1; + return ( + + ({ + id: l.supply.id, + measure: l.supply.measure, + name: l.supply.name, + priority: l.priority, + quantity: l.quantity, + }))} + onDonate={handleDonate} + /> + {!isLastElement && } + + ); + })}
- +
-
+
); }; diff --git a/src/pages/Shelter/components/ShelterCategoryItems/ShelterCategoryItems.tsx b/src/pages/Shelter/components/ShelterCategoryItems/ShelterCategoryItems.tsx deleted file mode 100644 index edc16d09..00000000 --- a/src/pages/Shelter/components/ShelterCategoryItems/ShelterCategoryItems.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useContext, useMemo, useState } from 'react'; -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { cva } from 'class-variance-authority'; - -import { IShelterCategoryItemsProps } from './types'; -import { cn, getSupplyPriorityProps } from '@/lib/utils'; -import { CircleStatus, Chip } from '@/components'; -import { Button } from '@/components/ui/button'; -import { SupplyPriority } from '@/service/supply/types'; -import { SessionContext } from '@/contexts'; -import { Badge } from '@/components/ui/badge'; - -const ShelterCategoryItems = (props: IShelterCategoryItemsProps) => { - const { - priority = SupplyPriority.NotNeeded, - tags, - onSelectTag, - selectedTags = [], - } = props; - const { session } = useContext(SessionContext); - const [opened, setOpened] = useState(false); - const maxVisibleSupplies: number = 10; - const visibleSupplies = useMemo( - () => (opened ? tags : tags.slice(0, maxVisibleSupplies)), - [opened, tags] - ); - const { className: circleClassName, label } = useMemo( - () => getSupplyPriorityProps(priority), - [priority] - ); - - const Icon = opened ? ChevronUp : ChevronDown; - const btnLabel = opened ? 'Ver menos' : 'Ver todos'; - - const variants = cva('cursor-pointer', { - variants: { - variant: { - selected: 'border-4 border-blue-300', - default: 'border-4 border-gray-100', - }, - }, - defaultVariants: { - variant: 'default', - }, - }); - - return ( -
-
- -

- {label} ({tags.length}) -

-
-
- {visibleSupplies.map((tag, idx) => { - const tagProps = - session && - ['DistributionCenter', 'Admin'].includes(session.accessLevel) - ? { - onClick: () => (onSelectTag ? onSelectTag(tag) : undefined), - className: variants({ - className: circleClassName, - variant: selectedTags.includes(tag) - ? 'selected' - : 'default', - }), - } - : { - className: circleClassName, - }; - return ( -
- - {tag.quantity !== null && - tag.quantity !== undefined && - tag.quantity > 0 && ( - - {tag.quantity > 99 ? '99+' : tag.quantity} - - )} -
- ); - })} -
- - {tags.length > maxVisibleSupplies && ( -
- -
- )} -
- ); -}; - -export { ShelterCategoryItems }; diff --git a/src/pages/Shelter/components/ShelterCategoryItems/index.ts b/src/pages/Shelter/components/ShelterCategoryItems/index.ts deleted file mode 100644 index 78d7838d..00000000 --- a/src/pages/Shelter/components/ShelterCategoryItems/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ShelterCategoryItems } from './ShelterCategoryItems'; - -export { ShelterCategoryItems }; diff --git a/src/pages/Shelter/components/ShelterCategoryItems/types.ts b/src/pages/Shelter/components/ShelterCategoryItems/types.ts deleted file mode 100644 index ebb2caa8..00000000 --- a/src/pages/Shelter/components/ShelterCategoryItems/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SupplyPriority } from '@/service/supply/types'; - -export interface ITagItem { - label: string; - value: string; - quantity?: number | null; -} - -export interface IShelterCategoryItemsProps { - priority?: SupplyPriority; - tags: ITagItem[]; - selectedTags?: ITagItem[]; - onSelectTag?: (v: ITagItem) => void; -} diff --git a/src/pages/Shelter/components/ShelterCategoryList/ShelterCategoryList.tsx b/src/pages/Shelter/components/ShelterCategoryList/ShelterCategoryList.tsx new file mode 100644 index 00000000..a5ac597a --- /dev/null +++ b/src/pages/Shelter/components/ShelterCategoryList/ShelterCategoryList.tsx @@ -0,0 +1,57 @@ +import clsx from 'clsx'; + +import { SupplyMeasureMap, cn, getSupplyPriorityProps } from '@/lib/utils'; +import { ShelterCategoryListProps } from './types'; +import { CircleStatus } from '@/components'; +import { SupplyPriority } from '@/service/supply/types'; + +const ShelterCategoryList = (props: ShelterCategoryListProps) => { + const { items, name, onDonate } = props; + + return ( +
+
+ {name} ({items.length} items) +
+
+ {items + .sort((a, b) => b.priority - a.priority) + .map((item) => { + const { className } = getSupplyPriorityProps(item.priority); + return ( +
+
+ + {item.name} +
+
+ {item.quantity && ( + {`${ + item.quantity + } ${SupplyMeasureMap[item.measure]}`} + )} + +
+
+ ); + })} +
+
+ ); +}; + +export { ShelterCategoryList }; diff --git a/src/pages/Shelter/components/ShelterCategoryList/index.ts b/src/pages/Shelter/components/ShelterCategoryList/index.ts new file mode 100644 index 00000000..eb8b93f0 --- /dev/null +++ b/src/pages/Shelter/components/ShelterCategoryList/index.ts @@ -0,0 +1,3 @@ +import { ShelterCategoryList } from './ShelterCategoryList'; + +export { ShelterCategoryList }; diff --git a/src/pages/Shelter/components/ShelterCategoryList/types.ts b/src/pages/Shelter/components/ShelterCategoryList/types.ts new file mode 100644 index 00000000..2c37df3a --- /dev/null +++ b/src/pages/Shelter/components/ShelterCategoryList/types.ts @@ -0,0 +1,16 @@ +import { SupplyMeasure } from '@/hooks/useShelter/types'; +import { SupplyPriority } from '@/service/supply/types'; + +export interface ShelterCategoryListItemProps { + id: string; + name: string; + quantity?: number | null; + priority: SupplyPriority; + measure: SupplyMeasure; +} + +export interface ShelterCategoryListProps { + name: string; + onDonate: (item: ShelterCategoryListItemProps) => void; + items: ShelterCategoryListItemProps[]; +} diff --git a/src/pages/Shelter/components/index.ts b/src/pages/Shelter/components/index.ts index 78d7838d..eb8b93f0 100644 --- a/src/pages/Shelter/components/index.ts +++ b/src/pages/Shelter/components/index.ts @@ -1,3 +1,3 @@ -import { ShelterCategoryItems } from './ShelterCategoryItems'; +import { ShelterCategoryList } from './ShelterCategoryList'; -export { ShelterCategoryItems }; +export { ShelterCategoryList }; diff --git a/src/service/donationOrder/donationOrder.service.ts b/src/service/donationOrder/donationOrder.service.ts new file mode 100644 index 00000000..717fd2c9 --- /dev/null +++ b/src/service/donationOrder/donationOrder.service.ts @@ -0,0 +1,34 @@ +import { api } from '@/api'; +import { + ICreateDonateResponse, + ICreateDonationOrderProps, + IDonateOrderItem, +} from './types'; +import { IServerResponse } from '@/types'; +import { IPaginatedResponse } from '@/hooks/usePaginatedQuery/types'; + +const DonationOrderServices = { + store: async (payload: ICreateDonationOrderProps) => { + const { data } = await api.post>( + '/donation/order', + payload + ); + return data; + }, + getAll: async (shelterId: string) => { + const { data } = await api.get< + IServerResponse> + >('/donation/order', { + params: { shelterId }, + }); + return data; + }, + find: async (id: string) => { + const { data } = await api.get>( + `/donation/order/${id}` + ); + return data; + }, +}; + +export { DonationOrderServices }; diff --git a/src/service/donationOrder/index.ts b/src/service/donationOrder/index.ts new file mode 100644 index 00000000..0ba66e4a --- /dev/null +++ b/src/service/donationOrder/index.ts @@ -0,0 +1,3 @@ +import { DonationOrderServices } from './donationOrder.service'; + +export { DonationOrderServices }; diff --git a/src/service/donationOrder/types.ts b/src/service/donationOrder/types.ts new file mode 100644 index 00000000..cad3c9b7 --- /dev/null +++ b/src/service/donationOrder/types.ts @@ -0,0 +1,51 @@ +import { SupplyMeasure } from '@/hooks/useShelter/types'; + +export interface IDonateItem { + id: string; + quantity: number; +} + +export interface ICreateDonationOrderProps { + shelterId: string; + supplies: IDonateItem[]; +} + +export interface ICreateDonateResponse { + id: string; + userId: string; + shelterId: string; + status: string; + createdAt: string; + updatedAt?: string | null; +} + +export enum DonateOrderStatus { + Pending = 'Pending', + Canceled = 'Canceled', + Complete = 'Complete', +} + +export interface IDonateOrderItem { + id: string; + status: DonateOrderStatus; + userId: string; + shelter: IDonateOrderShelter; + donationOrderSupplies: IDonateOrderItemSupply[]; + createdAt: string; + updatedAt?: string | null; +} + +export interface IDonateOrderItemSupply { + quantity: number; + supply: IDonateOrderSupply; +} + +export interface IDonateOrderShelter { + id: string; + name: string; +} + +export interface IDonateOrderSupply { + name: string; + measure: SupplyMeasure; +} diff --git a/src/service/index.ts b/src/service/index.ts index b9e629ca..50795f9d 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -3,6 +3,7 @@ import { ShelterServices } from './shelter'; import { SupplyServices } from './supply'; import { ShelterSupplyServices } from './shelterSupply'; import { UserServices } from './users'; +import { DonationOrderServices } from './donationOrder'; export { UserServices, @@ -10,4 +11,5 @@ export { SupplyServices, ShelterServices, ShelterSupplyServices, + DonationOrderServices, }; diff --git a/src/service/shelterSupply/shelter-supply.service.ts b/src/service/shelterSupply/shelter-supply.service.ts index aef057cb..04c617f6 100644 --- a/src/service/shelterSupply/shelter-supply.service.ts +++ b/src/service/shelterSupply/shelter-supply.service.ts @@ -1,6 +1,10 @@ import { api } from '@/api'; import { IServerResponse } from '@/types'; -import { ICreateShelterSupply, IUpdateShelterSupply } from './types'; +import { + ICreateShelterSupply, + IShelterSupplyData, + IUpdateShelterSupply, +} from './types'; const ShelterSupplyServices = { update: async ( @@ -14,22 +18,18 @@ const ShelterSupplyServices = { ); return data; }, - updateMany: async ( - shelterId: string, - supplyIds: string[] - ): Promise => { - const { data } = await api.put( - `/shelter/supplies/${shelterId}/supplies/many`, - { - ids: supplyIds, - } - ); - return data; - }, create: async (payload: ICreateShelterSupply): Promise => { const { data } = await api.post('/shelter/supplies', payload); return data; }, + getAll: async ( + shelterId: string + ): Promise> => { + const { data } = await api.get>( + `/shelter/supplies/${shelterId}` + ); + return data; + }, }; export { ShelterSupplyServices }; diff --git a/src/service/shelterSupply/types.ts b/src/service/shelterSupply/types.ts index 6c4d48ba..c41da483 100644 --- a/src/service/shelterSupply/types.ts +++ b/src/service/shelterSupply/types.ts @@ -1,3 +1,4 @@ +import { SupplyMeasure } from '@/hooks/useShelter/types'; import { SupplyPriority } from '../supply/types'; export interface IShelterSupply { @@ -18,3 +19,25 @@ export type ICreateShelterSupply = Pick< IShelterSupply, 'priority' | 'shelterId' | 'supplyId' | 'quantity' >; + +export interface IShelterSupplyData { + priority: number; + quantity: number; + supply: IShelterSupplyDataSupply; + createdAt: string; + updatedAt?: string | null; +} + +export interface IShelterSupplyDataSupply { + id: string; + name: string; + measure: SupplyMeasure; + supplyCategory: IShelterSupplyDataSupplyCategory; + createdAt: string; + updatedAt?: string | null; +} + +export interface IShelterSupplyDataSupplyCategory { + id: string; + name: string; +} diff --git a/src/service/users/types.ts b/src/service/users/types.ts index 3977b590..28e76bfa 100644 --- a/src/service/users/types.ts +++ b/src/service/users/types.ts @@ -22,3 +22,7 @@ export interface ICreateUser { lastName: string; phone: string; } + +export interface IFindUserResponse { + exists: boolean; +} diff --git a/src/service/users/user.service.ts b/src/service/users/user.service.ts index a5d2f296..5e29658a 100644 --- a/src/service/users/user.service.ts +++ b/src/service/users/user.service.ts @@ -1,7 +1,7 @@ import { api } from '../../api'; import { IServerResponse } from '@/types'; -import { ICreateUser, IUpdateUser } from './types'; +import { ICreateUser, IFindUserResponse, IUpdateUser, IUser } from './types'; const UserServices = { create: async (payload: ICreateUser): Promise => { @@ -18,6 +18,15 @@ const UserServices = { ); return data; }, + find: async ( + field: keyof IUser, + value: string + ): Promise> => { + const { data } = await api.get>( + `/users/find/${field}/${value}` + ); + return data; + }, }; export { UserServices };