diff --git a/app/page.tsx b/app/page.tsx index 5c939cd..9a5164b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,13 @@ -import { DeliveryList } from '@/components/DeliveryList'; +import { WorkflowCards } from '@/components/deliveries/WorkflowCards'; export default function Home() { return (
+
+
+ +
+

diff --git a/components/deliveries/WorkflowCards.tsx b/components/deliveries/WorkflowCards.tsx new file mode 100644 index 0000000..5d48072 --- /dev/null +++ b/components/deliveries/WorkflowCards.tsx @@ -0,0 +1,98 @@ +'use client'; + +import { useMemo } from 'react'; +import { useWorkflowCards } from '@/hooks/useWorkflowCards'; +import type { Delivery } from '@/types/delivery'; + +function getStatusColor(status: string) { + switch (status) { + case 'DELIVERED': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'IN_TRANSIT': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + case 'PENDING': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; + case 'ACCEPTED': + return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200'; + case 'CANCELLED': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; + } +} + +function formatDate(dateString: string) { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +export function WorkflowCards() { + const { data, isLoading, error } = useWorkflowCards(); + + const cards = useMemo(() => data ?? [], [data]); + + if (isLoading) { + return

Loading workflow cards...
; + } + + if (error) { + return
Error loading workflow cards: {error.message}
; + } + + if (!cards.length) { + return
No workflow cards available.
; + } + + return ( +
+ {cards.map((delivery: Delivery) => ( +
+
+
+
+

{delivery.trackingNumber}

+ + {delivery.status} + +
+ +
+

+ From: {delivery.origin} +

+

+ To: {delivery.destination} +

+
+ + {delivery.landmark ? ( +
+ + Landmark + +

+ {delivery.landmark} +

+
+ ) : null} +
+ +
+

Escrow: {delivery.escrowStatus}

+ {delivery.amount ?

Amount: {delivery.amount}

: null} +

Created: {formatDate(delivery.createdAt)}

+
+
+
+ ))} +
+ ); +} diff --git a/components/deliveries/__tests__/WorkflowCards.test.tsx b/components/deliveries/__tests__/WorkflowCards.test.tsx new file mode 100644 index 0000000..6a4f824 --- /dev/null +++ b/components/deliveries/__tests__/WorkflowCards.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { WorkflowCards } from '@/components/deliveries/WorkflowCards'; +import { deliveryWorkflowService } from '@/services/deliveryWorkflowService'; +import type { Delivery } from '@/types/delivery'; + +jest.mock('@/services/deliveryWorkflowService', () => ({ + deliveryWorkflowService: { + getWorkflowCards: jest.fn(), + }, +})); + +const mockedService = deliveryWorkflowService as jest.Mocked; + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +describe('WorkflowCards', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a landmark badge and address details when landmark data exists', async () => { + const deliveries: Delivery[] = [ + { + id: 'delivery-1', + trackingNumber: 'TRACK-100', + senderId: 'sender-1', + status: 'IN_TRANSIT', + origin: '123 Main St, Nairobi', + destination: '456 River Rd, Mombasa', + landmark: 'Near the central market', + escrowStatus: 'LOCKED', + amount: 120, + currency: 'USD', + createdAt: '2026-05-28T08:00:00Z', + updatedAt: '2026-05-28T08:00:00Z', + }, + ]; + + mockedService.getWorkflowCards.mockResolvedValue(deliveries); + + render(, { wrapper: createWrapper() }); + + await waitFor(() => { + expect(screen.getByText(/Near the central market/i)).toBeInTheDocument(); + }); + + expect(screen.getByText('Landmark')).toBeInTheDocument(); + expect(screen.getByText('123 Main St, Nairobi')).toBeInTheDocument(); + }); + + it('renders cards without crashing when landmark is null', async () => { + const deliveries: Delivery[] = [ + { + id: 'delivery-2', + trackingNumber: 'TRACK-200', + senderId: 'sender-2', + status: 'PENDING', + origin: '789 West St, Kisumu', + destination: '101 East Ave, Nakuru', + landmark: null, + escrowStatus: 'NOT_LOCKED', + amount: 80, + currency: 'USD', + createdAt: '2026-05-28T08:00:00Z', + updatedAt: '2026-05-28T08:00:00Z', + }, + ]; + + mockedService.getWorkflowCards.mockResolvedValue(deliveries); + + render(, { wrapper: createWrapper() }); + + await waitFor(() => { + expect(screen.getByText('TRACK-200')).toBeInTheDocument(); + }); + + expect(screen.getByText('789 West St, Kisumu')).toBeInTheDocument(); + expect(screen.getByText('101 East Ave, Nakuru')).toBeInTheDocument(); + }); +}); diff --git a/hooks/useWorkflowCards.ts b/hooks/useWorkflowCards.ts new file mode 100644 index 0000000..c7a4d9b --- /dev/null +++ b/hooks/useWorkflowCards.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; +import { deliveryWorkflowService } from '@/services/deliveryWorkflowService'; +import type { Delivery } from '@/types/delivery'; + +export function useWorkflowCards() { + return useQuery({ + queryKey: ['workflow-cards'], + queryFn: () => deliveryWorkflowService.getWorkflowCards(), + }); +} diff --git a/services/deliveryWorkflowService.ts b/services/deliveryWorkflowService.ts new file mode 100644 index 0000000..108b063 --- /dev/null +++ b/services/deliveryWorkflowService.ts @@ -0,0 +1,9 @@ +import { apiClient } from './api'; +import type { Delivery } from '../types/delivery'; + +export const deliveryWorkflowService = { + getWorkflowCards: async (): Promise => { + const { data } = await apiClient.get('/deliveries'); + return data; + }, +};