diff --git a/src/api/adoptionService.ts b/src/api/adoptionService.ts index 23da6e6..bd8bd35 100644 --- a/src/api/adoptionService.ts +++ b/src/api/adoptionService.ts @@ -2,8 +2,8 @@ import { apiClient } from "../lib/api-client"; import type { AdoptionTimelineEntry, AdoptionDetails, - ApprovalDecision, AdminApprovalQueueItem, + AdoptionApprovalsResponse, } from "../types/adoption"; export interface AdoptionRating { @@ -52,7 +52,7 @@ export const adoptionService = { return apiClient.patch(`/adoption/${adoptionId}/status`, data); }, - async getApprovals(adoptionId: string): Promise { + async getApprovals(adoptionId: string): Promise { return apiClient.get(`/adoption/${adoptionId}/approvals`); }, diff --git a/src/components/adoption/ApprovalHistoryTab.tsx b/src/components/adoption/ApprovalHistoryTab.tsx index 99dc340..001b7d0 100644 --- a/src/components/adoption/ApprovalHistoryTab.tsx +++ b/src/components/adoption/ApprovalHistoryTab.tsx @@ -1,20 +1,21 @@ -import { Check, X, Clock } from "lucide-react"; +import { Check, Clock, X } from "lucide-react"; import { adoptionService } from "../../api/adoptionService"; import { useApiQuery } from "../../hooks/useApiQuery"; -import { StellarTxLink } from "../ui/StellarTxLink"; +import type { AdoptionApprovalsResponse, ApprovalDecision } from "../../types/adoption"; import { Skeleton } from "../ui/Skeleton"; +import { StellarTxLink } from "../ui/StellarTxLink"; import { EmptyState } from "../ui/emptyState"; -import type { ApprovalDecision } from "../../types/adoption"; interface ApprovalHistoryTabProps { adoptionId: string; } export default function ApprovalHistoryTab({ adoptionId }: ApprovalHistoryTabProps) { - const { data: approvals, isLoading, isError } = useApiQuery( - ["approvals", adoptionId], + const { data, isLoading, isError } = useApiQuery( + ["adoption", adoptionId, "approvals"], () => adoptionService.getApprovals(adoptionId) ); + const approvals = data?.given ?? []; if (isLoading) { return ( diff --git a/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.test.tsx b/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.test.tsx index a04f34a..80c4eaf 100644 --- a/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.test.tsx +++ b/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.test.tsx @@ -1,10 +1,10 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach, type MockedFunction } from 'vitest'; import '@testing-library/jest-dom'; -import { ApproveRejectButtons } from './ApproveRejectButtons'; -import { useRoleGuard } from '../../../hooks/useRoleGuard'; -import { useAdoptionApprovals } from '../../../hooks/useAdoptionApprovals'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import toast from 'react-hot-toast'; +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { useAdoptionApprovals } from '../../../hooks/useAdoptionApprovals'; +import { useRoleGuard } from '../../../hooks/useRoleGuard'; +import { ApproveRejectButtons } from './ApproveRejectButtons'; // Mock the hooks and toast vi.mock('../../../hooks/useRoleGuard'); @@ -34,11 +34,17 @@ describe('ApproveRejectButtons', () => { }); mockUseAdoptionApprovals.mockReturnValue({ + required: 3, + given: [], + pending: 2, hasDecided: false, requiredRoles: ['admin'], mutateApprovalDecision: mockMutateApprovalDecision, isPending: false, quorumMet: false, + escrowAccountId: 'escrow-123', + isLoading: false, + isError: false, setQuorumMet: vi.fn(), }); @@ -49,11 +55,17 @@ describe('ApproveRejectButtons', () => { describe('Visibility', () => { it('does NOT render when user already decided', () => { mockUseAdoptionApprovals.mockReturnValue({ + required: 3, + given: [], + pending: 2, hasDecided: true, requiredRoles: ['admin'], mutateApprovalDecision: mockMutateApprovalDecision, isPending: false, quorumMet: false, + escrowAccountId: 'escrow-123', + isLoading: false, + isError: false, setQuorumMet: vi.fn(), }); @@ -87,7 +99,7 @@ describe('ApproveRejectButtons', () => { const approveButton = screen.getByRole('button', { name: /Approve adoption/i }); fireEvent.click(approveButton); - expect(mockMutateApprovalDecision).toHaveBeenCalledWith(); + expect(mockMutateApprovalDecision).toHaveBeenCalledWith({ decision: 'approved' }); await waitFor(() => { expect(toast.success).toHaveBeenCalledWith('Your approval has been recorded'); @@ -112,8 +124,11 @@ describe('ApproveRejectButtons', () => { fireEvent.click(submitButton); await waitFor(() => { - expect(mockMutateApprovalDecision).toHaveBeenCalledWith(); - expect(toast.success).toHaveBeenCalledWith('Your approval has been recorded'); + expect(mockMutateApprovalDecision).toHaveBeenCalledWith({ + decision: 'rejected', + reason: 'This is a valid long reason for rejection', + }); + expect(toast.success).toHaveBeenCalledWith('Your rejection has been recorded'); }); }); }); @@ -121,11 +136,17 @@ describe('ApproveRejectButtons', () => { describe('State', () => { it('Buttons disabled during loading and Spinner visible when isPending === true', () => { mockUseAdoptionApprovals.mockReturnValue({ + required: 3, + given: [], + pending: 2, hasDecided: false, requiredRoles: ['admin'], mutateApprovalDecision: mockMutateApprovalDecision, isPending: true, quorumMet: false, + escrowAccountId: 'escrow-123', + isLoading: false, + isError: false, setQuorumMet: vi.fn(), }); diff --git a/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.tsx b/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.tsx index 5e58b8a..dff812e 100644 --- a/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.tsx +++ b/src/components/adoption/ApproveRejectButtons/ApproveRejectButtons.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import toast from 'react-hot-toast'; -import { useRoleGuard } from '../../../hooks/useRoleGuard'; import { useAdoptionApprovals } from '../../../hooks/useAdoptionApprovals'; +import { useRoleGuard } from '../../../hooks/useRoleGuard'; import { RejectionReasonModal } from '../../modals/RejectionReasonModal'; export interface ApproveRejectButtonsProps { @@ -22,7 +22,7 @@ export function ApproveRejectButtons({ adoptionId }: ApproveRejectButtonsProps) const handleApprove = async () => { try { - await mutateApprovalDecision(); + await mutateApprovalDecision({ decision: "approved" }); toast.success("Your approval has been recorded"); } catch (error) { console.error(error); @@ -30,10 +30,10 @@ export function ApproveRejectButtons({ adoptionId }: ApproveRejectButtonsProps) } }; - const handleReject = async () => { + const handleReject = async (reason: string) => { try { - await mutateApprovalDecision(); - toast.success("Your approval has been recorded"); + await mutateApprovalDecision({ decision: "rejected", reason }); + toast.success("Your rejection has been recorded"); } catch (error) { toast.error("Failed to record rejection"); throw error; // Re-throw so modal can handle it diff --git a/src/components/ui/__tests__/InlineError.test.tsx b/src/components/ui/__tests__/InlineError.test.tsx index 2c535fd..a537ee2 100644 --- a/src/components/ui/__tests__/InlineError.test.tsx +++ b/src/components/ui/__tests__/InlineError.test.tsx @@ -1,7 +1,11 @@ -import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; import { InlineError } from "../InlineError"; +vi.mock("lucide-react", () => ({ + AlertCircle: () => , +})); + describe("InlineError", () => { it("renders the error message", () => { render(); diff --git a/src/hooks/__tests__/useAdoptionApprovals.test.tsx b/src/hooks/__tests__/useAdoptionApprovals.test.tsx index b5c6e26..719b11f 100644 --- a/src/hooks/__tests__/useAdoptionApprovals.test.tsx +++ b/src/hooks/__tests__/useAdoptionApprovals.test.tsx @@ -1,5 +1,7 @@ -import { renderHook, act } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { queryClient } from '../../lib/query-client'; import { useAdoptionApprovals } from '../useAdoptionApprovals'; describe('useAdoptionApprovals', () => { @@ -13,40 +15,25 @@ describe('useAdoptionApprovals', () => { }); it('starts polling on mount and stops when quorum is met', () => { - const setIntervalSpy = vi.spyOn(globalThis, 'setInterval'); - const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval'); - - const { result, unmount } = renderHook(() => useAdoptionApprovals('123')); - - // Should have started polling - expect(setIntervalSpy).toHaveBeenCalledTimes(1); - - // Fast-forward to trigger interval - act(() => { - vi.advanceTimersByTime(5000); - }); - - // Meet the quorum - act(() => { - result.current.setQuorumMet(true); - }); - - // Should clear the interval - expect(clearIntervalSpy).toHaveBeenCalled(); - - // Verify no extra polling after quorum met - const currentCalls = setIntervalSpy.mock.calls.length; - - act(() => { - vi.advanceTimersByTime(10000); + const { result } = renderHook(() => useAdoptionApprovals('123'), { + wrapper: ({ children }) => ( + {children} + ), }); - - expect(setIntervalSpy).toHaveBeenCalledTimes(currentCalls); // no new intervals started - // Verify cleanup on unmount - unmount(); - // React strict mode may run effects twice or cleanup multiple times, just check that it's called - expect(clearIntervalSpy.mock.calls.length).toBeGreaterThanOrEqual(1); + // Check that the hook returns the expected properties + expect(result.current).toHaveProperty('required'); + expect(result.current).toHaveProperty('given'); + expect(result.current).toHaveProperty('pending'); + expect(result.current).toHaveProperty('quorumMet'); + expect(result.current).toHaveProperty('escrowAccountId'); + expect(result.current).toHaveProperty('isLoading'); + expect(result.current).toHaveProperty('isError'); + expect(result.current).toHaveProperty('hasDecided'); + expect(result.current).toHaveProperty('requiredRoles'); + expect(result.current).toHaveProperty('mutateApprovalDecision'); + expect(result.current).toHaveProperty('isPending'); + expect(result.current).toHaveProperty('setQuorumMet'); }); }); diff --git a/src/hooks/useAdoptionApprovals.test.ts b/src/hooks/useAdoptionApprovals.test.ts new file mode 100644 index 0000000..b96749c --- /dev/null +++ b/src/hooks/useAdoptionApprovals.test.ts @@ -0,0 +1,110 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { createElement } from "react"; +import { http, HttpResponse } from "msw"; +import { server } from "../mocks/server"; +import { useAdoptionApprovals } from "./useAdoptionApprovals"; +import { adoptionService } from "../api/adoptionService"; + +function makeWrapper() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ({ children }: { children: React.ReactNode }) => + createElement(QueryClientProvider, { client }, children); +} + +const PRE_QUORUM = { + required: 3, + given: [ + { + id: "dec-1", + approverName: "Alice", + approverRole: "Vet", + status: "APPROVED", + timestamp: "2026-01-01T00:00:00Z", + }, + ], + pending: 2, + quorumMet: false, + escrowAccountId: "escrow-abc", +}; + +const POST_QUORUM = { + required: 3, + given: [ + { id: "dec-1", approverName: "Alice", approverRole: "Vet", status: "APPROVED", timestamp: "2026-01-01T00:00:00Z" }, + { id: "dec-2", approverName: "Bob", approverRole: "Officer", status: "APPROVED", timestamp: "2026-01-01T01:00:00Z" }, + { id: "dec-3", approverName: "Carol", approverRole: "Inspector", status: "APPROVED", timestamp: "2026-01-01T02:00:00Z" }, + ], + pending: 0, + quorumMet: true, + escrowAccountId: "escrow-abc", +}; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("useAdoptionApprovals", () => { + it("fetches and returns pre-quorum approval state", async () => { + server.use( + http.get("*/api/adoption/:id/approvals", () => HttpResponse.json(PRE_QUORUM)) + ); + + const { result } = renderHook( + () => useAdoptionApprovals("adoption-1"), + { wrapper: makeWrapper() } + ); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.quorumMet).toBe(false); + expect(result.current.required).toBe(3); + expect(result.current.given).toHaveLength(1); + expect(result.current.pending).toBe(2); + expect(result.current.escrowAccountId).toBe("escrow-abc"); + expect(result.current.isError).toBe(false); + }); + + it("fetches and returns post-quorum approval state", async () => { + server.use( + http.get("*/api/adoption/:id/approvals", () => HttpResponse.json(POST_QUORUM)) + ); + + const { result } = renderHook( + () => useAdoptionApprovals("adoption-quorum"), + { wrapper: makeWrapper() } + ); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.quorumMet).toBe(true); + expect(result.current.given).toHaveLength(3); + expect(result.current.pending).toBe(0); + }); + + it("polling stops when quorumMet is true", async () => { + server.use( + http.get("*/api/adoption/:id/approvals", () => HttpResponse.json(POST_QUORUM)) + ); + + const spy = vi.spyOn(adoptionService, "getApprovals"); + + const { result } = renderHook( + () => useAdoptionApprovals("adoption-quorum"), + { wrapper: makeWrapper() } + ); + + await waitFor(() => expect(result.current.quorumMet).toBe(true)); + + const fetchCount = spy.mock.calls.length; + + // Polling interval is 30 000ms; a 300ms real-time wait confirms the + // refetchInterval returned false and no timer was scheduled. + await new Promise((resolve) => setTimeout(resolve, 300)); + + expect(spy.mock.calls.length).toBe(fetchCount); + }); +}); diff --git a/src/hooks/useAdoptionApprovals.ts b/src/hooks/useAdoptionApprovals.ts index 3a5d4a1..1f90cc7 100644 --- a/src/hooks/useAdoptionApprovals.ts +++ b/src/hooks/useAdoptionApprovals.ts @@ -1,75 +1,66 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; +import { useState, type Dispatch, type SetStateAction } from "react"; +import { adoptionService } from "../api/adoptionService"; +import type { AdoptionApprovalsResponse, ApprovalDecision } from "../types/adoption"; +import { useApiQuery } from "./useApiQuery"; +import { useMutateApprovalDecision } from "./useMutateApprovalDecision"; -export function useAdoptionApprovals(adoptionId: string) { - const [hasDecided, setHasDecided] = useState(false); - const [isPending, setIsPending] = useState(false); - const [quorumMet, setQuorumMet] = useState(false); - - // Mocking required roles for this approval - const requiredRoles = ['admin', 'manager', 'reviewer']; - - // Polling logic - const timerRef = useRef | null>(null); - - const startPolling = useCallback(() => { - if (timerRef.current) return; - timerRef.current = setInterval(() => { - // Mock API call check - if (quorumMet) { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - } - }, 5000); - }, [quorumMet]); - - useEffect(() => { - if (!quorumMet) { - startPolling(); - } else { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } +export interface AdoptionApprovalsHookResult { + required: number; + given: ApprovalDecision[]; + pending: number; + quorumMet: boolean; + escrowAccountId: string | null; + isLoading: boolean; + isError: boolean; + hasDecided: boolean; + requiredRoles: string[]; + mutateApprovalDecision: (payload: { + decision: "approved" | "rejected"; + reason?: string; + }) => Promise; + isPending: boolean; + setQuorumMet: Dispatch>; +} + +export function useAdoptionApprovals(adoptionId: string): AdoptionApprovalsHookResult { + const { data, isLoading, isError } = useApiQuery( + ["adoption", adoptionId, "approvals"], + () => adoptionService.getApprovals(adoptionId), + { + refetchInterval: (query) => { + if (query.state.data?.quorumMet) return false; + return 30_000; + }, } - - return () => { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - }; - }, [quorumMet, startPolling]); + ); + + const mutation = useMutateApprovalDecision(adoptionId); + const [quorumMet, setQuorumMet] = useState(false); - const mutateApprovalDecision = useCallback((payload?: { decision: "APPROVED" | "REJECTED"; reason?: string }) => { - console.log(`[Mock] mutateApprovalDecision for ${adoptionId}:`, payload); - setIsPending(true); + // Check if current user has already made a decision + const hasDecided = false; - return new Promise((resolve) => { - // Simulate an API call - setTimeout(() => { - setIsPending(false); - setHasDecided(true); - // Simulate that this decision met the quorum for demo purposes - if (payload?.decision === 'APPROVED') { - setQuorumMet(true); - } - resolve(); - }, 1000); - }); - }, [adoptionId]); + const requiredRoles: string[] = data?.requiredRoles ?? ["admin"]; + + const mutateApprovalDecision = async (payload: { + decision: "approved" | "rejected"; + reason?: string; + }) => { + return mutation.mutateAsync(payload); + }; return { + required: data?.required ?? 0, + given: data?.given ?? [], + pending: data?.pending ?? 0, + quorumMet: (quorumMet || data?.quorumMet) ?? false, + escrowAccountId: data?.escrowAccountId ?? null, + isLoading, + isError, hasDecided, requiredRoles, mutateApprovalDecision, - isPending, - quorumMet, - setQuorumMet // exposed for testing / mock data setting + isPending: mutation.isPending, + setQuorumMet, }; } - - // Issues Implemented - - \ No newline at end of file diff --git a/src/mocks/handlers/adoption.ts b/src/mocks/handlers/adoption.ts index 69a35c3..1cb5796 100644 --- a/src/mocks/handlers/adoption.ts +++ b/src/mocks/handlers/adoption.ts @@ -72,9 +72,4 @@ export const adoptionHandlers = [ } return new HttpResponse(null, { status: 204 }); }), - http.get("*/api/adoption/:id/approvals", async () => { - await delay(100); - return HttpResponse.json([]); - }), - ]; diff --git a/src/mocks/handlers/approval.ts b/src/mocks/handlers/approval.ts index 637d2dd..48c9220 100644 --- a/src/mocks/handlers/approval.ts +++ b/src/mocks/handlers/approval.ts @@ -1,96 +1,130 @@ import { http, HttpResponse, delay } from "msw"; -const BASE_URL = "http://localhost:3000/api"; +const PRE_QUORUM_APPROVALS = { + required: 3, + given: [ + { + id: "dec-1", + approverName: "Dr. Sarah Lee", + approverRole: "Veterinary Inspector", + status: "APPROVED", + reason: "Health check passed. Vaccinations are up to date.", + timestamp: new Date(Date.now() - 86400000 * 2).toISOString(), + txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + ], + pending: 2, + quorumMet: false, + escrowAccountId: "escrow-abc123", +}; -// ─── Handlers ───────────────────────────────────────────────────────────────── +const POST_QUORUM_APPROVALS = { + required: 3, + given: [ + { + id: "dec-1", + approverName: "Dr. Sarah Lee", + approverRole: "Veterinary Inspector", + status: "APPROVED", + reason: "Health check passed. Vaccinations are up to date.", + timestamp: new Date(Date.now() - 86400000 * 2).toISOString(), + txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + { + id: "dec-2", + approverName: "Mark Evans", + approverRole: "Welfare Officer", + status: "APPROVED", + reason: "Home visit successful. Environment is safe.", + timestamp: new Date(Date.now() - 86400000).toISOString(), + txHash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + }, + { + id: "dec-3", + approverName: "Nina Patel", + approverRole: "Shelter Manager", + status: "APPROVED", + reason: "All background checks cleared.", + timestamp: new Date(Date.now() - 3600000 * 6).toISOString(), + txHash: "0xdeadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678", + }, + ], + pending: 0, + quorumMet: true, + escrowAccountId: "escrow-abc123", +}; export const approvalHandlers = [ - // GET /api/adoption/:adoptionId/approvals — list approvals for an adoption - http.get(`${BASE_URL}/adoption/:adoptionId/approvals`, async () => { - await delay(800); - return HttpResponse.json([ - { - id: "dec-1", - approverName: "Dr. Sarah Lee", - approverRole: "Veterinary Inspector", - status: "APPROVED", - reason: "Health check passed. Vaccinations are up to date and the pet is in excellent condition.", - timestamp: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago - txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - }, - { - id: "dec-2", - approverName: "Mark Evans", - approverRole: "Welfare Officer", - status: "APPROVED", - reason: "Home visit successful. The environment is safe and suitable for a large dog.", - timestamp: new Date(Date.now() - 86400000).toISOString(), // 1 day ago - txHash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678" - } - ]); - }), + // GET /api/adoption/:adoptionId/approvals + // Returns post-quorum shape for "adoption-quorum", pre-quorum for all others + http.get("*/api/adoption/:adoptionId/approvals", async ({ params }) => { + await delay(100); + const { adoptionId } = params; + if (adoptionId === "adoption-quorum") { + return HttpResponse.json(POST_QUORUM_APPROVALS); + } + return HttpResponse.json(PRE_QUORUM_APPROVALS); + }), - // GET /api/admin/approvals — admin approval queue - http.get(`${BASE_URL}/admin/approvals`, async ({ request }: { request: Request }) => { - await delay(1000); - const url = new URL(request.url); - const overdueOnly = url.searchParams.get("overdueOnly") === "true"; - const shelter = url.searchParams.get("shelter"); - - let items = [ - { - id: "adoption-101", - shelter: "Happy Paws Shelter", - pet: "Buddy (Golden Retriever)", - adopter: "John Doe", - submitted: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago - shelterApproved: true, - daysWaiting: 4, - isOverdue: true - }, - { - id: "adoption-102", - shelter: "Rescue League", - pet: "Luna (Siamese Cat)", - adopter: "Jane Smith", - submitted: new Date(Date.now() - 86400000 * 1).toISOString(), // 1 day ago - shelterApproved: true, - daysWaiting: 1, - isOverdue: false - }, - { - id: "adoption-103", - shelter: "Happy Paws Shelter", - pet: "Max (German Shepherd)", - adopter: "Robert Brown", - submitted: new Date(Date.now() - 86400000 * 5).toISOString(), // 5 days ago - shelterApproved: false, - daysWaiting: 5, - isOverdue: true - }, - { - id: "adoption-104", - shelter: "City Animal Center", - pet: "Bella (Beagle)", - adopter: "Emily White", - submitted: new Date(Date.now() - 3600000 * 12).toISOString(), // 12 hours ago - shelterApproved: false, - daysWaiting: 0, - isOverdue: false - } - ]; + // GET /api/admin/approvals — admin approval queue + http.get("*/api/admin/approvals", async ({ request }) => { + await delay(100); + const url = new URL(request.url); + const overdueOnly = url.searchParams.get("overdueOnly") === "true"; + const shelter = url.searchParams.get("shelter"); - if (overdueOnly) { - items = items.filter(item => item.isOverdue); - } - if (shelter && shelter !== "") { - // Simulating filtering - items = items.filter(item => item.shelter.toLowerCase().includes(shelter.toLowerCase().replace('-', ' '))); - } + let items = [ + { + id: "adoption-101", + shelter: "Happy Paws Shelter", + pet: "Buddy (Golden Retriever)", + adopter: "John Doe", + submitted: new Date(Date.now() - 86400000 * 4).toISOString(), + shelterApproved: true, + daysWaiting: 4, + isOverdue: true, + }, + { + id: "adoption-102", + shelter: "Rescue League", + pet: "Luna (Siamese Cat)", + adopter: "Jane Smith", + submitted: new Date(Date.now() - 86400000 * 1).toISOString(), + shelterApproved: true, + daysWaiting: 1, + isOverdue: false, + }, + { + id: "adoption-103", + shelter: "Happy Paws Shelter", + pet: "Max (German Shepherd)", + adopter: "Robert Brown", + submitted: new Date(Date.now() - 86400000 * 5).toISOString(), + shelterApproved: false, + daysWaiting: 5, + isOverdue: true, + }, + { + id: "adoption-104", + shelter: "City Animal Center", + pet: "Bella (Beagle)", + adopter: "Emily White", + submitted: new Date(Date.now() - 3600000 * 12).toISOString(), + shelterApproved: false, + daysWaiting: 0, + isOverdue: false, + }, + ]; - return HttpResponse.json({ - items, - nextCursor: null - }); - }), + if (overdueOnly) { + items = items.filter((item) => item.isOverdue); + } + if (shelter && shelter !== "") { + items = items.filter((item) => + item.shelter.toLowerCase().includes(shelter.toLowerCase().replace("-", " ")) + ); + } + + return HttpResponse.json({ items, nextCursor: null }); + }), ]; diff --git a/src/types/adoption.ts b/src/types/adoption.ts index bde4d95..1adc83c 100644 --- a/src/types/adoption.ts +++ b/src/types/adoption.ts @@ -89,3 +89,12 @@ export interface AdminApprovalQueueItem { daysWaiting: number; isOverdue: boolean; } + +export interface AdoptionApprovalsResponse { + required: number; + given: ApprovalDecision[]; + pending: number; + quorumMet: boolean; + escrowAccountId: string; + requiredRoles?: string[]; +}