diff --git a/.gitignore b/.gitignore index 00bba9b..695b6e4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# instructions +instructions/ +instructions/* \ No newline at end of file diff --git a/src/@types/escrow.entity.ts b/src/@types/escrow.entity.ts index 0773572..d54ee9d 100644 --- a/src/@types/escrow.entity.ts +++ b/src/@types/escrow.entity.ts @@ -1,4 +1,4 @@ -import { CreatedAt, UpdatedAt } from "./dates.entity"; +import type { CreatedAt, UpdatedAt } from "./dates.entity"; export type MilestoneStatus = "completed" | "approved" | "pending"; @@ -87,3 +87,8 @@ export type EditMilestonesPayload = { escrow: EscrowPayload; signer: string; }; + +export interface BalanceItem { + address: string; + balance: number; +} diff --git a/src/components/modules/escrow/hooks/my-escrows.hook.ts b/src/components/modules/escrow/hooks/my-escrows.hook.ts index 8c5654b..62e89c7 100644 --- a/src/components/modules/escrow/hooks/my-escrows.hook.ts +++ b/src/components/modules/escrow/hooks/my-escrows.hook.ts @@ -2,7 +2,7 @@ import { useGlobalAuthenticationStore, useGlobalBoundedStore, } from "@/core/store/data"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useRef, useCallback } from "react"; interface useMyEscrowsProps { type: string; @@ -19,6 +19,9 @@ const useMyEscrows = ({ type }: useMyEscrowsProps) => { const [expandedRows, setExpandedRows] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(15); + const [isLoading, setIsLoading] = useState(false); + const fetchingRef = useRef(false); + const lastFetchKey = useRef(""); const totalPages = Math.ceil(totalEscrows / itemsPerPage); @@ -31,15 +34,40 @@ const useMyEscrows = ({ type }: useMyEscrowsProps) => { ); }, [escrows, currentPage, itemsPerPage]); + const memoizedFetchEscrows = useCallback(async () => { + if (!address || fetchingRef.current) return; + const fetchKey = `${address}-${type}`; + if (fetchKey === lastFetchKey.current) return; + try { + fetchingRef.current = true; + lastFetchKey.current = fetchKey; + setIsLoading(true); + await fetchAllEscrows({ address, type }); + } catch (error) { + console.error("[MyEscrows] Error fetching escrows:", error); + } finally { + setIsLoading(false); + fetchingRef.current = false; + } + }, [address, type, fetchAllEscrows]); + useEffect(() => { - const fetchEscrows = async () => { - if (address) { - fetchAllEscrows({ address, type }); - } + let timeoutId: NodeJS.Timeout; + + const debouncedFetch = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + memoizedFetchEscrows(); + }, 100); }; - fetchEscrows(); - }, []); + debouncedFetch(); + + return () => { + clearTimeout(timeoutId); + fetchingRef.current = false; + }; + }, [memoizedFetchEscrows]); const toggleRowExpansion = (rowId: string) => { setExpandedRows((prev) => @@ -66,6 +94,7 @@ const useMyEscrows = ({ type }: useMyEscrowsProps) => { itemsPerPageOptions, toggleRowExpansion, expandedRows, + isLoading, }; }; diff --git a/src/components/modules/escrow/services/escrow.service.ts b/src/components/modules/escrow/services/escrow.service.ts new file mode 100644 index 0000000..02a7579 --- /dev/null +++ b/src/components/modules/escrow/services/escrow.service.ts @@ -0,0 +1,75 @@ +import type { + Escrow, + EscrowPayload, + BalanceItem, +} from "@/@types/escrow.entity"; +import { + getAllEscrowsByUser, + updateEscrow, + addEscrow, +} from "../server/escrow.firebase"; +import { getBalance } from "./get-balance.service"; + +export const fetchAllEscrows = async ({ + address, + type = "approver", +}: { + address: string; + type: string; +}): Promise => { + const escrowsByUser = await getAllEscrowsByUser({ address, type }); + const contractIds = escrowsByUser.data.map( + (escrow: Escrow) => escrow.contractId, + ); + + if (!Array.isArray(contractIds)) { + throw new Error("contractIds is not a valid array."); + } + + const response = await getBalance(address, contractIds); + const balances = response.data as BalanceItem[]; + + return Promise.all( + escrowsByUser.data.map(async (escrow: Escrow) => { + const matchedBalance = balances.find( + (item) => item.address === escrow.contractId, + ); + + const plainBalance = matchedBalance ? matchedBalance.balance : 0; + const currentBalance = escrow.balance ? Number(escrow.balance) : 0; + + if (currentBalance !== plainBalance) { + await updateEscrow({ + escrowId: escrow.id, + payload: { balance: String(plainBalance) }, + }); + escrow.balance = String(plainBalance); + } + + return escrow; + }), + ); +}; + +export const addNewEscrow = async ( + payload: EscrowPayload, + address: string, + contractId: string, +): Promise => { + const response = await addEscrow({ payload, address, contractId }); + return response?.data; +}; + +export const updateExistingEscrow = async ({ + escrowId, + payload, +}: { + escrowId: string; + payload: Partial; +}): Promise => { + const response = await updateEscrow({ + escrowId, + payload: { ...payload, balance: String(payload.balance || 0) }, + }); + return response?.data; +}; diff --git a/src/components/modules/escrow/ui/dialogs/EditMilestonesDialog.tsx b/src/components/modules/escrow/ui/dialogs/EditMilestonesDialog.tsx index 0c4a9d7..10e1d36 100644 --- a/src/components/modules/escrow/ui/dialogs/EditMilestonesDialog.tsx +++ b/src/components/modules/escrow/ui/dialogs/EditMilestonesDialog.tsx @@ -68,8 +68,11 @@ const EditMilestonesDialog = ({ {milestones.map((milestone, index) => ( - <> -
+
+
{milestone.flag ? ( Approved ) : ( @@ -117,7 +120,7 @@ const EditMilestonesDialog = ({ Add Item )} - +
))}
diff --git a/src/components/modules/escrow/ui/dialogs/EscrowDetailDialog.tsx b/src/components/modules/escrow/ui/dialogs/EscrowDetailDialog.tsx index c276000..bd30235 100644 --- a/src/components/modules/escrow/ui/dialogs/EscrowDetailDialog.tsx +++ b/src/components/modules/escrow/ui/dialogs/EscrowDetailDialog.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import useEscrowDetailDialog from "./hooks/escrow-detail-dialog.hook"; -import { Escrow } from "@/@types/escrow.entity"; +import type { Escrow } from "@/@types/escrow.entity"; import { Card, CardContent } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { useFormatUtils } from "@/utils/hook/format.hook"; @@ -38,13 +38,13 @@ import { Handshake, Wallet, } from "lucide-react"; -import SkeletonMilestones from "./utils/SkeletonMilestones"; import EditMilestonesDialog from "./EditMilestonesDialog"; import { SuccessReleaseDialog, SuccessResolveDisputeDialog, } from "./SuccessDialog"; import { toast } from "@/hooks/toast.hook"; +import { useEscrowDialogs } from "./hooks/use-escrow-dialogs.hook"; interface EscrowDetailDialogProps { isDialogOpen: boolean; @@ -58,6 +58,8 @@ const EscrowDetailDialog = ({ setSelectedEscrow, }: EscrowDetailDialogProps) => { const selectedEscrow = useGlobalBoundedStore((state) => state.selectedEscrow); + const dialogStates = useEscrowDialogs(); + const activeTab = useEscrowBoundedStore((state) => state.activeTab); const { handleClose, @@ -72,64 +74,13 @@ const EscrowDetailDialog = ({ const { distributeEscrowEarningsSubmit } = useDistributeEarningsEscrowDialogHook(); - - const setIsResolveDisputeDialogOpen = useEscrowBoundedStore( - (state) => state.setIsResolveDisputeDialogOpen, - ); - const { handleOpen } = useResolveDisputeEscrowDialogHook({ - setIsResolveDisputeDialogOpen, + setIsResolveDisputeDialogOpen: dialogStates.resolveDispute.setIsOpen, }); - const { changeMilestoneStatusSubmit } = useChangeStatusEscrowDialogHook(); const { startDisputeSubmit } = useStartDisputeEscrowDialogHook(); const { changeMilestoneFlagSubmit } = useChangeFlagEscrowDialogHook(); - const isSecondDialogOpen = useEscrowBoundedStore( - (state) => state.isSecondDialogOpen, - ); - const setIsSecondDialogOpen = useEscrowBoundedStore( - (state) => state.setIsSecondDialogOpen, - ); - const setIsQRDialogOpen = useEscrowBoundedStore( - (state) => state.setIsQRDialogOpen, - ); - const isQRDialogOpen = useEscrowBoundedStore((state) => state.isQRDialogOpen); - - const isChangingStatus = useEscrowBoundedStore( - (state) => state.isChangingStatus, - ); - - const isStartingDispute = useEscrowBoundedStore( - (state) => state.isStartingDispute, - ); - - const isResolveDisputeDialogOpen = useEscrowBoundedStore( - (state) => state.isResolveDisputeDialogOpen, - ); - - const setIsEditMilestoneDialogOpen = useEscrowBoundedStore( - (state) => state.setIsEditMilestoneDialogOpen, - ); - const isEditMilestoneDialogOpen = useEscrowBoundedStore( - (state) => state.isEditMilestoneDialogOpen, - ); - const isSuccessReleaseDialogOpen = useEscrowBoundedStore( - (state) => state.isSuccessReleaseDialogOpen, - ); - const setIsSuccessReleaseDialogOpen = useEscrowBoundedStore( - (state) => state.setIsSuccessReleaseDialogOpen, - ); - - const isSuccessResolveDisputeDialogOpen = useEscrowBoundedStore( - (state) => state.isSuccessResolveDisputeDialogOpen, - ); - const setIsSuccessResolveDisputeDialogOpen = useEscrowBoundedStore( - (state) => state.setIsSuccessResolveDisputeDialogOpen, - ); - - const activeTab = useEscrowBoundedStore((state) => state.activeTab); - const { formatAddress, formatText, formatDollar, formatDateFromFirebase } = useFormatUtils(); const { copyText, copiedKeyId } = useCopyUtils(); @@ -196,7 +147,9 @@ const EscrowDetailDialog = ({ -

+
+ {formatAddress(selectedEscrow.contractId)} + +
- + )} {userRolesInEscrow.includes("disputeResolver") && @@ -395,79 +365,73 @@ const EscrowDetailDialog = ({ {/* Milestones */}
- {isChangingStatus || isStartingDispute ? ( - - ) : ( -
- - {selectedEscrow.milestones.map((milestone, index) => ( -
- {milestone.flag ? ( - - Approved - - ) : ( - + Milestones + + + {selectedEscrow.milestones.map( + (milestone, milestoneIndex) => ( +
+ {milestone.flag ? ( + Approved + ) : ( + + {milestone.status} + + )} + + + + {userRolesInEscrow.includes("serviceProvider") && + activeTab === "serviceProvider" && + milestone.status !== "completed" && + !milestone.flag && ( + )} - - - {userRolesInEscrow.includes("serviceProvider") && - activeTab === "serviceProvider" && - milestone.status !== "completed" && - !milestone.flag && ( - - )} - - {userRolesInEscrow.includes("approver") && - activeTab === "approver" && - milestone.status === "completed" && - !milestone.flag && ( - - )} -
- ))} - - -
+ {userRolesInEscrow.includes("approver") && + activeTab === "approver" && + milestone.status === "completed" && + !milestone.flag && ( + + )} +
+ ), )} + +
@@ -499,7 +463,7 @@ const EscrowDetailDialog = ({