diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/index.html b/index.html index 7649bb4..14851f4 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + diff --git a/src/App.tsx b/src/App.tsx index ca5860b..8b2bdf3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { Navigate, Outlet, Route, Routes } from 'react-router-dom' import LoginRoute from './routes/Login' import OrdersRoute from './routes/Orders/Index' import ProfileRoute from './routes/Profile' -import { ProtectedRoute } from './components/ProtectedRoute' +import { ProtectedRoute } from './components/ProtectedRoute.tsx' import { Header } from './components/Header' import { BottomNav } from './components/BottomNav' import { useAuth } from './hooks/useAuth' diff --git a/src/api/mockData.ts b/src/api/mockData.ts index e381c8e..245b454 100644 --- a/src/api/mockData.ts +++ b/src/api/mockData.ts @@ -44,6 +44,8 @@ export const mockOrders: Order[] = [ requiresIdCheck: true, requiresPaymentCheck: true, createdAt: minutesAgo(28), + acceptedAt: minutesAgo(32), + startedAt: minutesAgo(30), customer: { name: 'John Doe', phone: '(555) 246-8135', @@ -66,6 +68,10 @@ export const mockOrders: Order[] = [ requiresIdCheck: true, requiresPaymentCheck: true, createdAt: minutesAgo(125), + acceptedAt: minutesAgo(120), + startedAt: minutesAgo(118), + arrivedAt: minutesAgo(110), + completedAt: minutesAgo(105), customer: { name: 'Alice Lee', phone: '(555) 678-9012', diff --git a/src/api/orders.ts b/src/api/orders.ts index 6766aca..e2a9a6a 100644 --- a/src/api/orders.ts +++ b/src/api/orders.ts @@ -23,6 +23,25 @@ export async function acceptOrder(orderId: string): Promise { if (!existing) { throw new Error('Order not found') } + existing.acceptedAt = new Date().toISOString() + existing.status = 'ASSIGNED' + return existing + }, + ) +} + +export async function startOrder(orderId: string): Promise { + return safeRequest( + async () => { + const response = await apiClient.post(`/orders/${orderId}/start`) + return response.data + }, + async () => { + const existing = mockOrders.find((order) => order.id === orderId) + if (!existing) { + throw new Error('Order not found') + } + existing.startedAt = new Date().toISOString() existing.status = 'IN_PROGRESS' return existing }, @@ -40,6 +59,7 @@ export async function arriveOrder(orderId: string): Promise { if (!existing) { throw new Error('Order not found') } + existing.arrivedAt = new Date().toISOString() existing.status = 'ARRIVED' return existing }, @@ -49,7 +69,9 @@ export async function arriveOrder(orderId: string): Promise { export async function completeOrder(orderId: string, signature?: string): Promise { return safeRequest( async () => { - const response = await apiClient.post(`/orders/${orderId}/complete`, { signature }) + const response = await apiClient.post(`/orders/${orderId}/complete`, { + proof: signature ? { signatureUrl: signature } : undefined, + }) return response.data }, async () => { @@ -57,6 +79,7 @@ export async function completeOrder(orderId: string, signature?: string): Promis if (!existing) { throw new Error('Order not found') } + existing.completedAt = new Date().toISOString() existing.status = 'COMPLETED' return existing }, diff --git a/src/components/VerifyChecklist.tsx b/src/components/VerifyChecklist.tsx index 335518f..941a49e 100644 --- a/src/components/VerifyChecklist.tsx +++ b/src/components/VerifyChecklist.tsx @@ -1,35 +1,55 @@ interface VerifyChecklistProps { idChecked: boolean paymentChecked: boolean + requiresIdCheck: boolean + requiresPaymentCheck: boolean onChange: (next: { idChecked: boolean; paymentChecked: boolean }) => void } -export function VerifyChecklist({ idChecked, paymentChecked, onChange }: VerifyChecklistProps): JSX.Element { +export function VerifyChecklist({ + idChecked, + paymentChecked, + requiresIdCheck, + requiresPaymentCheck, + onChange, +}: VerifyChecklistProps): JSX.Element { return (

Required Verifications

- - + {requiresIdCheck ? ( + + ) : ( +
+ ID verification not required +
+ )} + {requiresPaymentCheck ? ( + + ) : ( +
+ Payment verification not required +
+ )}
) } diff --git a/src/hooks/useOrders.tsx b/src/hooks/useOrders.tsx index 478792e..b0634f1 100644 --- a/src/hooks/useOrders.tsx +++ b/src/hooks/useOrders.tsx @@ -7,8 +7,7 @@ import { useMemo, useState, } from 'react' -import { acceptOrder, completeOrder, getOrders } from '../api/orders' -import { arriveOrder } from '../api/orders' +import { acceptOrder, completeOrder, getOrders, startOrder, arriveOrder } from '../api/orders' import { Order } from '../types' import { useSocket } from './useSocket' import { useToast } from './useToast' @@ -18,6 +17,7 @@ interface OrdersContextValue { orders: Order[] refresh: () => Promise accept: (orderId: string) => Promise + markStarted: (orderId: string) => Promise markArrived: (orderId: string) => Promise markComplete: (orderId: string, signature?: string) => Promise isFetching: boolean @@ -78,6 +78,12 @@ export function OrdersProvider({ children }: PropsWithChildren): JSX.Element { push({ title: 'Order accepted', description: order.number, variant: 'success' }) }, [mergeOrder, push]) + const markStarted = useCallback(async (orderId: string) => { + const order = await startOrder(orderId) + mergeOrder(order) + push({ title: 'Delivery started', description: order.number, variant: 'info' }) + }, [mergeOrder, push]) + const markArrived = useCallback(async (orderId: string) => { const order = await arriveOrder(orderId) mergeOrder(order) @@ -91,8 +97,8 @@ export function OrdersProvider({ children }: PropsWithChildren): JSX.Element { }, [mergeOrder, push]) const value = useMemo( - () => ({ orders, refresh, accept, markArrived, markComplete, isFetching }), - [accept, isFetching, markArrived, markComplete, orders, refresh], + () => ({ orders, refresh, accept, markStarted, markArrived, markComplete, isFetching }), + [accept, isFetching, markArrived, markComplete, markStarted, orders, refresh], ) return {children} diff --git a/src/main.tsx b/src/main.tsx index ac2a5f1..9fa5ebf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' -import App from './App' +import App from './App.tsx' import './styles/globals.css' import { AuthProvider } from './hooks/useAuth' import { ToastProvider } from './hooks/useToast' diff --git a/src/routes/Orders/CompletedOrderCard.tsx b/src/routes/Orders/CompletedOrderCard.tsx index 755711b..4807f7b 100644 --- a/src/routes/Orders/CompletedOrderCard.tsx +++ b/src/routes/Orders/CompletedOrderCard.tsx @@ -7,18 +7,11 @@ interface CompletedOrderCardProps { order: Order expanded: boolean onToggle: (order: Order) => void - onArrive: (orderId: string) => Promise - onComplete: (orderId: string, signature: string) => Promise } -export function CompletedOrderCard({ - order, - expanded, - onToggle, - onArrive, - onComplete, -}: CompletedOrderCardProps): JSX.Element { - const deliveredTime = formatTimeOfDay(order.createdAt) +export function CompletedOrderCard({ order, expanded, onToggle }: CompletedOrderCardProps): JSX.Element { + const deliveredAt = order.completedAt ?? order.createdAt + const deliveredTime = formatTimeOfDay(deliveredAt) return (
@@ -48,7 +41,7 @@ export function CompletedOrderCard({ {expanded ? (
- +
) : null}
diff --git a/src/routes/Orders/Index.tsx b/src/routes/Orders/Index.tsx index e4684ed..316963b 100644 --- a/src/routes/Orders/Index.tsx +++ b/src/routes/Orders/Index.tsx @@ -9,47 +9,73 @@ import { useToast } from '../../hooks/useToast' import { useAuth } from '../../hooks/useAuth' const tabConfig = [ - { id: 'pending', label: 'Pending' }, - { id: 'active', label: 'Active' }, + { id: 'assigned', label: 'Assigned' }, + { id: 'accepted', label: 'Accepted (In Progress)' }, { id: 'completed', label: 'Completed' }, ] type TabId = (typeof tabConfig)[number]['id'] +function isCompleted(order: Order): boolean { + return Boolean(order.completedAt) || order.status === 'COMPLETED' +} + +function isActive(order: Order): boolean { + if (isCompleted(order)) return false + if (order.status === 'ARRIVED' || order.status === 'IN_PROGRESS') return true + if (order.startedAt) return true + if (order.acceptedAt) return true + if (order.status === 'ASSIGNED' && (order.acceptedAt || order.startedAt)) return true + return false +} + export default function OrdersRoute(): JSX.Element { - const { orders, accept, markArrived, markComplete } = useOrders() + const { orders, accept, markStarted, markArrived, markComplete } = useOrders() const { push } = useToast() const { driver } = useAuth() - const [activeTab, setActiveTab] = useState('pending') + const [activeTab, setActiveTab] = useState('assigned') const [expandedCompletedId, setExpandedCompletedId] = useState(undefined) const [completedVisibleCount, setCompletedVisibleCount] = useState(10) const segmented = useMemo(() => { - const pending = orders.filter((order) => order.status === 'NEW') - const active = orders.filter((order) => { - if (order.status !== 'IN_PROGRESS' && order.status !== 'ARRIVED') { - return false + const assigned: Order[] = [] + const accepted: Order[] = [] + const completed: Order[] = [] + const driverId = driver?.id + + orders.forEach((order) => { + if (!driverId || order.assignedDriverId !== driverId) { + return } - if (!driver) return true + if (isCompleted(order)) { + completed.push(order) + return + } + + if (isActive(order)) { + accepted.push(order) + return + } - return order.assignedDriverId === driver.id + assigned.push(order) }) - const completed = orders - .filter((order) => { - if (order.status !== 'COMPLETED') return false - if (!driver) return true - return order.assignedDriverId === driver.id - }) - .sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ) - return { pending, active, completed } - }, [driver, orders]) + + completed.sort((a, b) => { + const aDate = a.completedAt ?? a.createdAt + const bDate = b.completedAt ?? b.createdAt + return new Date(bDate).getTime() - new Date(aDate).getTime() + }) + + accepted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) + assigned.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) + + return { assigned, accepted, completed } + }, [driver?.id, orders]) const listForTab = useMemo(() => { - if (activeTab === 'pending') return segmented.pending - if (activeTab === 'active') return segmented.active + if (activeTab === 'assigned') return segmented.assigned + if (activeTab === 'accepted') return segmented.accepted return segmented.completed }, [activeTab, segmented]) @@ -62,7 +88,11 @@ export default function OrdersRoute(): JSX.Element { const handleAccept = async (order: Order) => { await accept(order.id) - setActiveTab('active') + setActiveTab('accepted') + } + + const handleStart = async (orderId: string) => { + await markStarted(orderId) } const handleArrive = async (orderId: string) => { @@ -96,11 +126,14 @@ export default function OrdersRoute(): JSX.Element { return (
({ ...tab, badge: tab.id === 'pending' ? segmented.pending.length : undefined }))} + tabs={tabConfig.map((tab) => ({ + ...tab, + badge: tab.id === 'assigned' ? segmented.assigned.length : undefined, + }))} activeId={activeTab} onChange={(id) => setActiveTab(id as TabId)} /> - {activeTab === 'pending' ? ( + {activeTab === 'assigned' ? (
{listForTab.length === 0 ? ( @@ -112,13 +145,19 @@ export default function OrdersRoute(): JSX.Element { )}
- ) : activeTab === 'active' ? ( + ) : activeTab === 'accepted' ? (
{listForTab.length === 0 ? (

No orders in this state.

) : ( listForTab.map((order) => ( - + )) )}
@@ -135,12 +174,8 @@ export default function OrdersRoute(): JSX.Element { order={order} expanded={expandedCompletedId === order.id} onToggle={(next) => - setExpandedCompletedId((current) => - current === next.id ? undefined : next.id, - ) + setExpandedCompletedId((current) => (current === next.id ? undefined : next.id)) } - onArrive={handleArrive} - onComplete={handleComplete} /> ))}
diff --git a/src/routes/Orders/OrderCard.tsx b/src/routes/Orders/OrderCard.tsx index 6811b35..b8426db 100644 --- a/src/routes/Orders/OrderCard.tsx +++ b/src/routes/Orders/OrderCard.tsx @@ -11,8 +11,7 @@ interface OrderCardProps { } export function OrderCard({ order, onAccept, onSelect, isSelected }: OrderCardProps): JSX.Element { - const isPending = order.status === 'NEW' - const statusLabel = order.status === 'COMPLETED' ? 'Completed' : undefined + const showAccept = Boolean(onAccept) const isSelectable = Boolean(onSelect) return ( @@ -54,7 +53,7 @@ export function OrderCard({ order, onAccept, onSelect, isSelected }: OrderCardPr ))} ) : null} - {isPending && onAccept ? ( + {showAccept ? ( + ) : null} + {awaitingArrival ? ( + {!isCompleted ? ( + <> + { + setIdChecked(id) + setPaymentChecked(payment) + }} + /> +
+

Customer Signature

+ +
+ + + ) : ( +
Delivery completed · Signature on file.
+ )} - ) : order.status === 'COMPLETED' ? ( + ) : isCompleted ? (
Delivery completed · Signature on file.
) : null} diff --git a/src/styles/globals.css b/src/styles/globals.css index a93c382..5e4e4f8 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -823,6 +823,12 @@ button { background: rgba(16, 185, 129, 0.12); } +.verify-item.disabled { + cursor: default; + opacity: 0.65; + justify-content: flex-start; +} + .verify-checkbox { width: 24px; height: 24px; diff --git a/src/types.ts b/src/types.ts index c526dc3..3f19b9e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,10 @@ export interface Order { requiresIdCheck: boolean requiresPaymentCheck: boolean createdAt: string + acceptedAt?: string | null + startedAt?: string | null + arrivedAt?: string | null + completedAt?: string | null customer: Customer priority?: boolean items: OrderItem[]