diff --git a/src/components/ui/DocumentExpiryBadge.tsx b/src/components/ui/DocumentExpiryBadge.tsx
index 70d3989..be7d4cd 100644
--- a/src/components/ui/DocumentExpiryBadge.tsx
+++ b/src/components/ui/DocumentExpiryBadge.tsx
@@ -1,31 +1,72 @@
+import { AlertCircle } from 'lucide-react'
+
interface DocumentExpiryBadgeProps {
- expiresAt: string | null;
+ expiresAt: string | null
+ status: string
}
export function DocumentExpiryBadge({ expiresAt }: DocumentExpiryBadgeProps) {
- if (!expiresAt) return null;
-
- const now = new Date();
- const expiry = new Date(expiresAt);
- const msUntilExpiry = expiry.getTime() - now.getTime();
- const daysUntilExpiry = msUntilExpiry / (1000 * 60 * 60 * 24);
-
- const config =
- daysUntilExpiry < 0
- ? { label: 'Expired', textClass: 'text-red-700', bgClass: 'bg-red-100' }
- : daysUntilExpiry <= 30
- ? { label: 'Expiring soon', textClass: 'text-amber-700', bgClass: 'bg-amber-100' }
- : {
- label: `Expires ${expiry.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`,
- textClass: 'text-blue-700',
- bgClass: 'bg-blue-100',
- };
+ // No badge if expiresAt is null
+ if (!expiresAt) {
+ return null
+ }
+
+ const expiryDate = new Date(expiresAt)
+ // Reset expiry time to midnight for consistent day comparison
+ expiryDate.setHours(0, 0, 0, 0)
+
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+
+ const daysUntilExpiry = Math.ceil(
+ (expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
+ )
+
+ // Determine badge state and styling
+ let badgeConfig: {
+ label: string
+ bgClass: string
+ textClass: string
+ tooltip: string
+ }
+
+ if (daysUntilExpiry < 0) {
+ // Expired
+ badgeConfig = {
+ label: 'Expired — re-upload required',
+ bgClass: 'bg-red-100',
+ textClass: 'text-red-700',
+ tooltip: `Expired on ${expiryDate.toLocaleDateString()}`,
+ }
+ } else if (daysUntilExpiry <= 7) {
+ // Expiring soon (within 7 days)
+ const dayLabel = daysUntilExpiry === 1 ? 'day' : 'days'
+ badgeConfig = {
+ label: `Expiring in ${daysUntilExpiry} ${dayLabel}`,
+ bgClass: 'bg-amber-100',
+ textClass: 'text-amber-700',
+ tooltip: `Expires on ${expiryDate.toLocaleDateString()}`,
+ }
+ } else {
+ // Not expiring soon - no badge
+ return null
+ }
return (
-
- {config.label}
-
- );
+
+
+
+
{badgeConfig.label}
+
+
+ {/* Tooltip */}
+
+
+ {badgeConfig.tooltip}
+
+
+
+ )
}
diff --git a/src/components/ui/__tests__/DocumentExpiryBadge.test.tsx b/src/components/ui/__tests__/DocumentExpiryBadge.test.tsx
new file mode 100644
index 0000000..9e72975
--- /dev/null
+++ b/src/components/ui/__tests__/DocumentExpiryBadge.test.tsx
@@ -0,0 +1,227 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { DocumentExpiryBadge } from '../DocumentExpiryBadge'
+
+// Mock current date to 2026-03-29 for consistent testing
+const MOCK_TODAY = new Date('2026-03-29')
+
+beforeEach(() => {
+ vi.useFakeTimers()
+ vi.setSystemTime(MOCK_TODAY)
+})
+
+afterEach(() => {
+ vi.useRealTimers()
+})
+
+describe('DocumentExpiryBadge', () => {
+ describe('null expiresAt', () => {
+ it('renders nothing when expiresAt is null', () => {
+ const { container } = render(
+
+ )
+ expect(container.firstChild).toBeNull()
+ })
+ })
+
+ describe('amber threshold (within 7 days)', () => {
+ it('shows amber badge for document expiring in 7 days', () => {
+ const expiryDate = new Date('2026-04-05') // 7 days from now
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-amber-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-amber-700')
+ })
+
+ it('shows amber badge for document expiring in 1 day', () => {
+ const expiryDate = new Date('2026-03-30') // 1 day from now
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-amber-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-amber-700')
+ })
+
+ it('shows amber badge for document expiring in 3 days', () => {
+ const expiryDate = new Date('2026-04-01') // 3 days from now
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-amber-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-amber-700')
+ })
+
+ it('includes AlertCircle icon in amber badge', () => {
+ const expiryDate = new Date('2026-04-01')
+ const { container } = render(
+
+ )
+
+ // Lucide icons render as SVG
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('shows correct tooltip for amber badge', () => {
+ const expiryDate = new Date('2026-04-01')
+ const { container } = render(
+
+ )
+
+ // Tooltip text should contain the expiry date
+ const tooltip = container.querySelector('.group-hover\\:opacity-100')
+ expect(tooltip?.textContent).toContain('4/1/2026')
+ })
+ })
+
+ describe('red threshold (expired)', () => {
+ it('shows red badge for expired document', () => {
+ const expiryDate = new Date('2026-03-28') // 1 day ago
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-red-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-red-700')
+ })
+
+ it('shows red badge for document expired months ago', () => {
+ const expiryDate = new Date('2025-12-25') // Months ago
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-red-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-red-700')
+ })
+
+ it('includes AlertCircle icon in red badge', () => {
+ const expiryDate = new Date('2026-03-28')
+ const { container } = render(
+
+ )
+
+ const svg = container.querySelector('svg')
+ expect(svg).toBeInTheDocument()
+ })
+
+ it('shows correct tooltip for red badge', () => {
+ const expiryDate = new Date('2026-03-15')
+ const { container } = render(
+
+ )
+
+ const tooltip = container.querySelector('.group-hover\\:opacity-100')
+ expect(tooltip?.textContent).toContain('3/15/2026')
+ })
+ })
+
+ describe('no badge states', () => {
+ it('renders nothing for document expiring in 8+ days', () => {
+ const expiryDate = new Date('2026-04-10') // 12 days from now
+ const { container } = render(
+
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders nothing for document expiring far in future', () => {
+ const expiryDate = new Date('2027-12-31') // Almost 2 years away
+ const { container } = render(
+
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+ })
+
+ describe('edge cases', () => {
+ it('handles document expiring today (0 days)', () => {
+ const expiryDate = new Date('2026-03-29') // Today
+ const { container } = render(
+
+ )
+
+ const badgeContainer = container.querySelector('.bg-amber-100')
+ expect(badgeContainer).toBeInTheDocument()
+ expect(badgeContainer).toHaveClass('text-amber-700')
+ })
+
+ it('correctly pluralizes "day" vs "days"', () => {
+ // Test with 1 day
+ const { unmount: unmount1 } = render(
+
+ )
+ expect(screen.getByText(/Expiring in 1 day/)).toBeInTheDocument()
+ unmount1()
+
+ // Test with 2 days
+ render(
+
+ )
+ expect(screen.getByText(/Expiring in 2 days/)).toBeInTheDocument()
+ })
+
+ it('accepts ISO string format for expiresAt', () => {
+ const isoString = '2026-04-01T10:30:00Z'
+ render(
+
+ )
+
+ const badge = screen.getByText(/Expiring in 3 days/)
+ expect(badge).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/mocks/handlers/files.ts b/src/mocks/handlers/files.ts
index 9a3bc8a..c420ee8 100644
--- a/src/mocks/handlers/files.ts
+++ b/src/mocks/handlers/files.ts
@@ -15,6 +15,7 @@ interface Document {
adoptionId: string;
createdAt: string;
updatedAt: string;
+ expiresAt?: string | null;
}
interface UploadDocumentsResponse {
@@ -36,6 +37,7 @@ const MOCK_DOCUMENTS: Document[] = [
adoptionId: "adoption-001",
createdAt: "2026-03-18T08:00:00.000Z",
updatedAt: "2026-03-18T08:00:00.000Z",
+ expiresAt: null,
},
{
id: "doc-002",
@@ -48,6 +50,20 @@ const MOCK_DOCUMENTS: Document[] = [
adoptionId: "adoption-001",
createdAt: "2026-03-18T08:30:00.000Z",
updatedAt: "2026-03-18T08:30:00.000Z",
+ expiresAt: "2026-04-01T23:59:59.000Z",
+ },
+ {
+ id: "doc-003",
+ fileName: "proof-of-residence.pdf",
+ fileUrl: "https://res.cloudinary.com/petad/image/upload/v1/adoptions/adoption-001/proof-of-residence.pdf",
+ publicId: "adoptions/adoption-001/proof-of-residence",
+ mimeType: "application/pdf",
+ size: 153600,
+ uploadedById: "user-owner-1",
+ adoptionId: "adoption-001",
+ createdAt: "2026-03-18T09:00:00.000Z",
+ updatedAt: "2026-03-18T09:00:00.000Z",
+ expiresAt: "2026-03-15T23:59:59.000Z",
},
];
@@ -81,6 +97,7 @@ export const filesHandlers = [
adoptionId: params.id as string,
createdAt: now,
updatedAt: now,
+ expiresAt: null,
};
return HttpResponse.json({