diff --git a/src/App.tsx b/src/App.tsx index 647355f..0aef4b2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { SettlementSummaryPage } from "./pages/SettlementSummaryPage"; import AdoptionTimelinePage from "./pages/AdoptionTimelinePage"; import ModalPreview from "./pages/ModalPreview"; import StatusPollingDemo from "./pages/StatusPollingDemo"; +import DisputeDetailPage from "./pages/DisputeDetailPage"; <<<<<<< feat/custody-timeline-page import CustodyTimelinePage from "./pages/CustodyTimelinePage"; ======= @@ -47,6 +48,7 @@ function App() { } /> } /> } /> + } /> {/* Custody Routes */} } /> diff --git a/src/components/dispute/DisputeResolutionSection.tsx b/src/components/dispute/DisputeResolutionSection.tsx new file mode 100644 index 0000000..4d680e3 --- /dev/null +++ b/src/components/dispute/DisputeResolutionSection.tsx @@ -0,0 +1,73 @@ +import type { FC } from "react"; +import { Dispute } from "../../types/dispute"; +import { SplitOutcomeChart } from "../escrow/SplitOutcomeChart"; +import { StellarTxLink } from "../escrow/StellarTxLink"; + +interface DisputeResolutionSectionProps { + dispute: Dispute; +} + +export const DisputeResolutionSection: FC = ({ + dispute, +}) => { + if (dispute.status !== "RESOLVED" || !dispute.resolution) { + return null; + } + + const { resolution } = dispute; + + return ( +
+

+ Resolution +

+ +
+ {/* Resolution Type Badge */} +
+ + {resolution.type} + +
+ + {/* Admin Note */} +
+

Admin Note

+

{resolution.adminNote}

+
+ + {/* Resolved By */} +
+

Resolved By

+

{resolution.resolvedBy}

+
+ + {/* Resolved At */} +
+

Resolved At

+

+ {new Date(resolution.resolvedAt).toLocaleString()} +

+
+ + {/* Split Chart if SPLIT */} + {resolution.type === "SPLIT" && resolution.distribution && ( +
+

+ Distribution +

+ +
+ )} + + {/* Stellar Tx Link */} +
+

+ Resolution Transaction +

+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/dispute/__tests__/DisputeResolutionSection.test.tsx b/src/components/dispute/__tests__/DisputeResolutionSection.test.tsx new file mode 100644 index 0000000..9a83bb8 --- /dev/null +++ b/src/components/dispute/__tests__/DisputeResolutionSection.test.tsx @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { DisputeResolutionSection } from "../DisputeResolutionSection"; +import type { Dispute } from "../../../types/dispute"; + +describe("DisputeResolutionSection", () => { + it("is hidden when dispute status is OPEN", () => { + const dispute: Dispute = { + id: "dispute-001", + adoptionId: "adoption-001", + raisedBy: "user-1", + reason: "test", + description: "test", + status: "OPEN", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + }; + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders resolution details when status is RESOLVED", () => { + const dispute: Dispute = { + id: "dispute-001", + adoptionId: "adoption-001", + raisedBy: "user-1", + reason: "test", + description: "test", + status: "RESOLVED", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + resolution: { + type: "REFUND", + adminNote: "Refund approved", + resolvedBy: "admin@example.com", + resolvedAt: "2026-03-25T14:30:00.000Z", + resolutionTxHash: "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + }, + }; + + render(); + + expect(screen.getByText("Resolution")).toBeInTheDocument(); + expect(screen.getByText("REFUND")).toBeInTheDocument(); + expect(screen.getByText("Refund approved")).toBeInTheDocument(); + expect(screen.getByText("admin@example.com")).toBeInTheDocument(); + expect(screen.getByTestId("stellar-tx-link")).toBeInTheDocument(); + }); + + it("renders SplitOutcomeChart when resolution type is SPLIT", () => { + const dispute: Dispute = { + id: "dispute-001", + adoptionId: "adoption-001", + raisedBy: "user-1", + reason: "test", + description: "test", + status: "RESOLVED", + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-01T00:00:00.000Z", + resolution: { + type: "SPLIT", + adminNote: "Split resolution", + resolvedBy: "admin@example.com", + resolvedAt: "2026-03-25T14:30:00.000Z", + resolutionTxHash: "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + distribution: [ + { recipient: "Shelter", amount: "50.00", percentage: 50 }, + { recipient: "Adopter", amount: "50.00", percentage: 50 }, + ], + }, + }; + + render(); + + expect(screen.getByText("SPLIT")).toBeInTheDocument(); + expect(screen.getByTestId("split-outcome-chart")).toBeInTheDocument(); + expect(screen.getByTestId("stellar-tx-link")).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/src/components/dispute/index.ts b/src/components/dispute/index.ts new file mode 100644 index 0000000..c384279 --- /dev/null +++ b/src/components/dispute/index.ts @@ -0,0 +1 @@ +export { DisputeResolutionSection } from "./DisputeResolutionSection"; \ No newline at end of file diff --git a/src/pages/DisputeDetailPage.tsx b/src/pages/DisputeDetailPage.tsx new file mode 100644 index 0000000..78bf7f6 --- /dev/null +++ b/src/pages/DisputeDetailPage.tsx @@ -0,0 +1,38 @@ +import { useParams } from "react-router-dom"; +import { DisputeResolutionSection } from "../components/dispute/DisputeResolutionSection"; + +export default function DisputeDetailPage() { + const { id } = useParams(); + + // TODO: Fetch dispute data using id + // For now, mock data + const dispute = { + id: id || "dispute-001", + status: "RESOLVED" as const, + resolution: { + type: "SPLIT" as const, + adminNote: "After reviewing the evidence, a split resolution was deemed fair.", + resolvedBy: "admin@example.com", + resolvedAt: "2026-03-25T14:30:00.000Z", + resolutionTxHash: "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + distribution: [ + { recipient: "Shelter", amount: "50.00", percentage: 50 }, + { recipient: "Adopter", amount: "50.00", percentage: 50 }, + ], + }, + }; + + return ( +
+
+

+ Dispute Details +

+ + {/* Dispute basic info would go here */} + + +
+
+ ); +} \ No newline at end of file diff --git a/src/types/dispute.ts b/src/types/dispute.ts new file mode 100644 index 0000000..1f3cd42 --- /dev/null +++ b/src/types/dispute.ts @@ -0,0 +1,28 @@ +export type DisputeStatus = "OPEN" | "UNDER_REVIEW" | "RESOLVED" | "CLOSED"; + +export type ResolutionType = "REFUND" | "RELEASE" | "SPLIT"; + +export interface DisputeResolution { + type: ResolutionType; + adminNote: string; + resolvedBy: string; + resolvedAt: string; + resolutionTxHash: string; + distribution?: { + recipient: string; + amount: string; + percentage: number; + }[]; +} + +export interface Dispute { + id: string; + adoptionId: string; + raisedBy: string; + reason: string; + description: string; + status: DisputeStatus; + resolution?: DisputeResolution | null; + createdAt: string; + updatedAt: string; +} \ No newline at end of file