Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
=======
Expand Down Expand Up @@ -47,6 +48,7 @@ function App() {
<Route path="/notifications" element={<NotificationPage />} />
<Route path="/adoption/:adoptionId/settlement" element={<SettlementSummaryPage />} />
<Route path="/adoption/:adoptionId/timeline" element={<AdoptionTimelinePage />} />
<Route path="/dispute/:id" element={<DisputeDetailPage />} />

{/* Custody Routes */}
<Route path="/custody/:custodyId/timeline" element={<CustodyTimelinePage />} />
Expand Down
73 changes: 73 additions & 0 deletions src/components/dispute/DisputeResolutionSection.tsx
Original file line number Diff line number Diff line change
@@ -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<DisputeResolutionSectionProps> = ({
dispute,
}) => {
if (dispute.status !== "RESOLVED" || !dispute.resolution) {
return null;
}

const { resolution } = dispute;

return (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Resolution
</h2>

<div className="space-y-4">
{/* Resolution Type Badge */}
<div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{resolution.type}
</span>
</div>

{/* Admin Note */}
<div>
<h3 className="text-sm font-medium text-gray-700">Admin Note</h3>
<p className="mt-1 text-sm text-gray-900">{resolution.adminNote}</p>
</div>

{/* Resolved By */}
<div>
<h3 className="text-sm font-medium text-gray-700">Resolved By</h3>
<p className="mt-1 text-sm text-gray-900">{resolution.resolvedBy}</p>
</div>

{/* Resolved At */}
<div>
<h3 className="text-sm font-medium text-gray-700">Resolved At</h3>
<p className="mt-1 text-sm text-gray-900">
{new Date(resolution.resolvedAt).toLocaleString()}
</p>
</div>

{/* Split Chart if SPLIT */}
{resolution.type === "SPLIT" && resolution.distribution && (
<div>
<h3 className="text-sm font-medium text-gray-700 mb-2">
Distribution
</h3>
<SplitOutcomeChart distribution={resolution.distribution} />
</div>
)}

{/* Stellar Tx Link */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-2">
Resolution Transaction
</h3>
<StellarTxLink txHash={resolution.resolutionTxHash} />
</div>
</div>
</div>
);
};
80 changes: 80 additions & 0 deletions src/components/dispute/__tests__/DisputeResolutionSection.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DisputeResolutionSection dispute={dispute} />);
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(<DisputeResolutionSection dispute={dispute} />);

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(<DisputeResolutionSection dispute={dispute} />);

expect(screen.getByText("SPLIT")).toBeInTheDocument();
expect(screen.getByTestId("split-outcome-chart")).toBeInTheDocument();
expect(screen.getByTestId("stellar-tx-link")).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/components/dispute/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DisputeResolutionSection } from "./DisputeResolutionSection";
38 changes: 38 additions & 0 deletions src/pages/DisputeDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-white py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Dispute Details
</h1>

{/* Dispute basic info would go here */}

<DisputeResolutionSection dispute={dispute} />
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions src/types/dispute.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading