diff --git a/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx index 6418d37e..1e2cb63d 100644 --- a/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx +++ b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx @@ -11,15 +11,21 @@ import { SupplyMeasureMap, cn } from '@/lib/utils'; import { useDonationOrder } from '@/hooks'; import { format } from 'date-fns'; import { Button } from '@/components/ui/button'; +import { useNavigate } from 'react-router-dom'; const DonationSuccess = React.forwardRef( (props, ref) => { const { donationOrderId, className = '', ...rest } = props; const { data: donation, loading } = useDonationOrder(donationOrderId); + const navigate = useNavigate(); if (loading) return ; + const handleRedirect = () => { + navigate(`/abrigo/${donation.shelter.id}/doacoes`); + }; + return (
@@ -71,7 +77,11 @@ const DonationSuccess = React.forwardRef(
-
diff --git a/src/hooks/useDonations/index.ts b/src/hooks/useDonations/index.ts new file mode 100755 index 00000000..65baa472 --- /dev/null +++ b/src/hooks/useDonations/index.ts @@ -0,0 +1,3 @@ +import { useDonations } from './useDonations'; + +export { useDonations }; diff --git a/src/hooks/useDonations/types.ts b/src/hooks/useDonations/types.ts new file mode 100755 index 00000000..b1c28fc8 --- /dev/null +++ b/src/hooks/useDonations/types.ts @@ -0,0 +1,25 @@ +export interface IUseDonationsData { + page: number; + perPage: number; + count: number; + results: IDonationsData[]; +} + +export interface IDonationsData { + id: string; + userId: string; + status: string; + shelter: { + id: string; + name: string; + }; + donationOrderSupplies: { + quantity: number; + supply: { + measure: string; + name: string; + }; + }; + createdAt: string; + updatedAt: string; +} diff --git a/src/hooks/useDonations/useDonations.tsx b/src/hooks/useDonations/useDonations.tsx new file mode 100755 index 00000000..2a378827 --- /dev/null +++ b/src/hooks/useDonations/useDonations.tsx @@ -0,0 +1,9 @@ +import { useFetch } from '../useFetch'; +import { PaginatedQueryPath } from '../usePaginatedQuery/paths'; +import { IUseDonationsData } from './types'; + +const useDonations = (shelterId: string) => { + return useFetch(`${PaginatedQueryPath.DonationOrder}`); +}; + +export { useDonations }; diff --git a/src/pages/DonationsHistory/DonationsHistory.tsx b/src/pages/DonationsHistory/DonationsHistory.tsx new file mode 100644 index 00000000..a445bcdc --- /dev/null +++ b/src/pages/DonationsHistory/DonationsHistory.tsx @@ -0,0 +1,155 @@ +import { Header, LoadingScreen } from '@/components'; +import { Button } from '@/components/ui/button'; +import { ChevronLeft } from 'lucide-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useShelter } from '@/hooks'; +import { IDonations, IDonationsPerDay, ViewOptions } from './types'; +import { useDonations } from '@/hooks/useDonations'; +import { useEffect, useState } from 'react'; +import { DonationsPerDay } from './components/DonationsPerDay'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +const DonationsHistory = () => { + const navigate = useNavigate(); + const params = useParams(); + const { shelterId = '-1' } = params; + const { data: shelter, loading: shelterLoading } = useShelter(shelterId); + const { data: shelterDonations, loading: donationsLoading } = + useDonations(shelterId); + const [donationsReceivedPerDay, setDonationsReceivedPerDay] = useState< + IDonationsPerDay | {} + >([]); + const [donationsGivenPerDay, setDonationsGivenPerDay] = useState< + IDonationsPerDay | {} + >([]); + + const [viewOption, setViewOption] = useState(ViewOptions.Donated); + + const toggleViewOption = () => { + setViewOption((prevOption) => + prevOption === ViewOptions.Donated + ? ViewOptions.Received + : ViewOptions.Donated + ); + }; + + const donationGroupedByDate = (donations: IDonations): IDonationsPerDay => { + return donations.reduce((acc, donation) => { + const date = donation.createdAt.split('T')[0]; + + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(donation); + + return acc; + }, {}); + }; + + const filterDonationsByCase = ( + donations: IDonationsPerDay, + shelterId: string + ) => { + const receivedDonations: IDonationsPerDay = {}; + const givenDonations: IDonationsPerDay = {}; + + Object.keys(donations).forEach((date) => { + receivedDonations[date] = donations[date].filter( + (donation) => donation.shelter.id === shelterId + ); + givenDonations[date] = donations[date].filter( + (donation) => donation.shelter.id !== shelterId + ); + }); + + return { receivedDonations, givenDonations }; + }; + + useEffect(() => { + if (!donationsLoading) { + const donationsPerDay = donationGroupedByDate(shelterDonations.results); + const { receivedDonations, givenDonations } = filterDonationsByCase( + donationsPerDay, + shelterId + ); + setDonationsGivenPerDay(givenDonations); + setDonationsReceivedPerDay(receivedDonations); + } + }, [donationsLoading]); + + if (!donationsLoading) { + const dailyDonations = { + donated: donationsGivenPerDay, + received: donationsReceivedPerDay, + }; + + const segmentedDonationsDisplay = Object.keys( + dailyDonations[viewOption] + ).map((day) => { + return ( +
+

+ {format(day, "dd 'de' MMMM yyyy ", { locale: ptBR })} +

+ +
+ ); + }); + + if (donationsLoading) return ; + + return ( +
+
navigate('/')} + > + + + } + /> +
+
+

+ Suas doações +

+
+
+
toggleViewOption()} + > +

+ Doado +

+
+
toggleViewOption()} + > +

+ Recebido +

+
+
+ {segmentedDonationsDisplay} +
+
+ ); + } +}; +export { DonationsHistory }; diff --git a/src/pages/DonationsHistory/components/ConfirmationDialog.tsx b/src/pages/DonationsHistory/components/ConfirmationDialog.tsx new file mode 100644 index 00000000..0c7da3ef --- /dev/null +++ b/src/pages/DonationsHistory/components/ConfirmationDialog.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { + Dialog, + DialogTrigger, + DialogPortal, + DialogOverlay, + DialogContent, + DialogFooter, + DialogTitle, + DialogDescription, +} from '../../../components/ui/dialog'; +import { Button } from '../../../components/ui/button'; + +interface ConfirmationDialogProps { + title: string; + description: string; + confirmLabel: string; + cancelLabel: string; + onConfirm: () => void; + onCancel: () => void; + triggerLabel: React.ReactNode; + Icon: React.ComponentType>; +} + +const ConfirmationDialog: React.FC = ({ + title, + description, + confirmLabel, + cancelLabel, + onConfirm, + + triggerLabel, + Icon, +}) => { + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + + + + + + +
+ {title} +
+ + {description} + + + + + +
+
+
+ ); +}; + +export { ConfirmationDialog }; diff --git a/src/pages/DonationsHistory/components/Donation.tsx b/src/pages/DonationsHistory/components/Donation.tsx new file mode 100644 index 00000000..6e910111 --- /dev/null +++ b/src/pages/DonationsHistory/components/Donation.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react'; +import { Printer, PackageCheck, CircleX } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { IDonationProps, ViewOptions } from '../types'; +import { Chip } from '@/components'; +import { DonationOrderServices } from '@/service/donationOrder/donationOrder.service'; +import { DonateOrderStatus } from '@/service/donationOrder/types'; +import { ConfirmationDialog } from './ConfirmationDialog'; + +const Donation = ({ viewOption, donation }: IDonationProps) => { + const [opened, setOpened] = useState(false); + const [status, setStatus] = useState(donation.status); + const [error, setError] = useState(null); + + const getDisplayDate = (status: string): DonateOrderStatus => { + if (status === DonateOrderStatus.Complete) { + return `Entregue no dia ${donation.createdAt.split('T')[0]} às + ${donation.createdAt.split('T')[1].slice(0, -5)}`; + } else if (status === DonateOrderStatus.Pending) { + return `Criado no dia ${donation.createdAt.split('T')[0]} às + ${donation.createdAt.split('T')[1].slice(0, -5)}`; + } else if (status === DonateOrderStatus.Canceled) { + return `Cancelado no dia ${donation.createdAt.split('T')[0]} às + ${donation.createdAt.split('T')[1].slice(0, -5)}`; + } + }; + + const [displayDate, setDisplayDate] = useState(getDisplayDate(status)); + + useEffect(() => { + const displayDate = getDisplayDate(status); + setDisplayDate(displayDate); + }, [status]); + + const Icon = !opened ? ChevronUp : ChevronDown; + const btnLabel = !opened ? 'Ocultar itens doados' : 'Mostrar itens doados'; + + //Creates list of all items to be displayed + const listOfItems = donation.items.map((item: string, index) => { + return ( +
  • + {`${item.quantity} ${ + item.supply.measure == 'Unit' ? 'unidade(s)' : item.supply.measure + } ${item.supply.name}`} +
  • + ); + }); + + const getStatusVariant = (status: string) => { + if (status === DonateOrderStatus.Complete) { + return { label: 'Entregue', color: '#A9CB9D' }; + } else if (status === DonateOrderStatus.Pending) { + return { label: 'Pendente', color: '#F69F9D' }; + } else { + return { label: 'Cancelado', color: '#D3D3D3' }; + } + }; + + const statusVariant = getStatusVariant(status); + + const handleConfirm = async () => { + try { + await DonationOrderServices.update(donation.donationId, { + status: DonateOrderStatus.Complete, + }); + setStatus(DonateOrderStatus.Complete); + } catch (err) { + setError('Failed to confirm the delivery. Please try again.'); + } + }; + + const handleCancel = async () => { + try { + await DonationOrderServices.update(donation.donationId, { + status: DonateOrderStatus.Canceled, + }); + setStatus(DonateOrderStatus.Canceled); + } catch (err) { + setError('Failed to cancel the delivery. Please try again.'); + } + }; + return ( + +
    + {viewOption == ViewOptions.Received ? 'Doação para' : 'Doação de'} + +
    +
    + {viewOption == ViewOptions.Received + ? donation.donatorName + : donation.shelterName} +
    +
    {displayDate}
    +
    + +
    + {opened &&
      {listOfItems}
    } +
    + + {status !== DonateOrderStatus.Complete && + status !== DonateOrderStatus.Canceled && ( + <> + {}} + triggerLabel={ + + Cancelar entrega + + } + Icon={CircleX} + /> + {}} + triggerLabel={ + + Confirmar entrega + + } + Icon={PackageCheck} + /> + + )} +
    +
    + ); +}; + +export { Donation }; diff --git a/src/pages/DonationsHistory/components/DonationsPerDay.tsx b/src/pages/DonationsHistory/components/DonationsPerDay.tsx new file mode 100644 index 00000000..4e694967 --- /dev/null +++ b/src/pages/DonationsHistory/components/DonationsPerDay.tsx @@ -0,0 +1,34 @@ +import { IDonationsPerDayProps } from '../types'; +import { Donation } from './Donation'; + +const DonationsPerDay = ({ donations, viewOption }: IDonationsPerDayProps) => { + const donationsOfDay = donations.map((donation) => { + donation = { + donationId: donation.id, + donatorName: donation.shelter.name, + donatorId: donation.userId, + shelterId: donation.shelter.id, + shelterName: donation.shelter.name, + status: donation.status, + createdAt: donation.createdAt, + updatedAt: donation.updatedAt || null, + items: donation.donationOrderSupplies, + }; + + return ( + + ); + }); + + return ( +
    +
    +
    {donationsOfDay}
    +
    + ); +}; +export { DonationsPerDay }; diff --git a/src/pages/DonationsHistory/index.ts b/src/pages/DonationsHistory/index.ts new file mode 100644 index 00000000..0e61b546 --- /dev/null +++ b/src/pages/DonationsHistory/index.ts @@ -0,0 +1,3 @@ +import { DonationsHistory } from './DonationsHistory'; + +export { DonationsHistory }; diff --git a/src/pages/DonationsHistory/types.ts b/src/pages/DonationsHistory/types.ts new file mode 100644 index 00000000..1229f76a --- /dev/null +++ b/src/pages/DonationsHistory/types.ts @@ -0,0 +1,25 @@ +import { IDonationsData } from '@/hooks/useDonations/types'; + +export type IDonations = IDonationsData[]; + +export interface IDonationsPerDay { + [date: string]: IDonations; +} + +export interface IDonationsInGivenDay { + donations: IDonations; +} + +export enum ViewOptions { + Donated = 'donated', + Received = 'received', +} +export interface IDonationsPerDayProps { + donations: IDonations; + viewOption: ViewOptions; +} + +export interface IDonationProps { + viewOption: ViewOptions; + donation: IDonationProps; +} diff --git a/src/pages/index.ts b/src/pages/index.ts index 8afdb97c..0c5f8a89 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -8,6 +8,7 @@ import { UpdateShelter } from './UpdateShelter'; import { PrivacyPolicy } from './PrivacyPolicy'; import { AboutUs } from './AboutUs'; import { Supporters } from './Supporters'; +import { DonationsHistory } from './DonationsHistory'; export { SignIn, @@ -20,4 +21,5 @@ export { PrivacyPolicy, AboutUs, Supporters, + DonationsHistory, }; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 6dd56608..35d09fa2 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -11,6 +11,7 @@ import { PrivacyPolicy, AboutUs, Supporters, + DonationsHistory, } from '@/pages'; const Routes = () => { @@ -24,6 +25,7 @@ const Routes = () => { path="/abrigo/:shelterId/item/cadastrar" element={} /> + } /> } /> } /> } /> diff --git a/src/service/donationOrder/donationOrder.service.ts b/src/service/donationOrder/donationOrder.service.ts index 717fd2c9..aba23f35 100644 --- a/src/service/donationOrder/donationOrder.service.ts +++ b/src/service/donationOrder/donationOrder.service.ts @@ -1,5 +1,6 @@ import { api } from '@/api'; import { + DonateOrderStatus, ICreateDonateResponse, ICreateDonationOrderProps, IDonateOrderItem, @@ -29,6 +30,13 @@ const DonationOrderServices = { ); return data; }, + update: async (id: string, payload: { status: DonateOrderStatus }) => { + const { data } = await api.put>( + `/donation/order/${id}`, + payload + ); + return data; + }, }; export { DonationOrderServices };