- {listForTab.length === 0 ? (
-
No orders in this state.
+ ) : activeTab === 'accepted' ? (
+
+ {acceptedOrders.length === 0 ? (
+
No accepted orders right now.
) : (
- listForTab.map((order) => (
-
- ))
+
+ {acceptedOrders.map((order) => (
+
+ ))}
+
)}
) : (
-
- {segmented.completed.length === 0 ? (
-
No orders in this state.
+
+ {outForDeliveryOrders.length === 0 ? (
+
No stops are currently in transit.
) : (
- <>
-
- {visibleCompletedOrders.map((order) => (
-
- setExpandedCompletedId((current) =>
- current === next.id ? undefined : next.id,
+
+ {outForDeliveryOrders.map((order) => (
+
+ ))}
+
+ )}
+
+
Recent deliveries
+ {segmented.completed.length === 0 ? (
+
No completed deliveries yet today.
+ ) : (
+ <>
+
+ {visibleCompletedOrders.map((order) => (
+
+ setExpandedCompletedId((current) =>
+ current === next.id ? undefined : next.id,
+ )
+ }
+ onArrive={handleArrive}
+ onComplete={handleComplete}
+ />
+ ))}
+
+ {canShowMoreCompleted ? (
+
- {canShowMoreCompleted ? (
-
- ) : null}
- >
- )}
+ >
+ See more
+
+ ) : null}
+ >
+ )}
+
)}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 6b63316..ad64b80 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -876,76 +876,123 @@ button {
box-shadow: 0 18px 40px rgba(79, 70, 229, 0.15);
margin-top: -2px;
}
+
.tabs {
display: flex;
- background: white;
- border-bottom: 1px solid #e5e7eb;
+ gap: 8px;
+ background: rgba(255, 255, 255, 0.9);
+ border-radius: 16px;
+ padding: 6px;
position: sticky;
- top: 0;
+ top: 16px;
z-index: 10;
+ box-shadow: 0 12px 30px rgba(79, 70, 229, 0.15);
+ backdrop-filter: blur(14px);
+ margin-top: -18px;
}
.tab {
flex: 1;
- padding: 15px;
+ padding: 14px 0;
text-align: center;
- font-weight: 500;
- color: #6b7280;
+ font-weight: 600;
+ color: #5b21b6;
cursor: pointer;
background: transparent;
border: none;
+ border-radius: 12px;
position: relative;
- font-size: 14px;
+ font-size: 13px;
+ letter-spacing: 0.4px;
+ transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.tab.active {
- color: #667eea;
+ color: white;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ box-shadow: 0 12px 24px rgba(102, 126, 234, 0.35);
}
.tab.active::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- height: 3px;
- background: #667eea;
+ display: none;
}
.tab-badge {
- background: #ef4444;
- color: white;
+ background: rgba(102, 126, 234, 0.18);
+ color: #4338ca;
font-size: 11px;
- padding: 2px 6px;
+ padding: 3px 8px;
border-radius: 999px;
- margin-left: 6px;
+ margin-left: 8px;
+ font-weight: 600;
}
.orders-page {
display: flex;
flex-direction: column;
+ gap: 20px;
min-height: 100%;
}
-.orders-content {
+.orders-hero {
+ position: relative;
+ overflow: hidden;
+ border-radius: 20px;
+ padding: 24px 24px 28px;
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.25));
+ backdrop-filter: blur(14px);
+ box-shadow: 0 24px 50px rgba(79, 70, 229, 0.2);
display: flex;
flex-direction: column;
- gap: 20px;
- padding: 20px;
+ gap: 16px;
+}
+
+.orders-hero::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(circle at 10% 10%, rgba(255, 255, 255, 0.45), transparent 60%);
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.orders-hero__badge {
+ position: relative;
+ align-self: flex-start;
+ padding: 6px 14px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.85);
+ color: #4c1d95;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
}
-.pending-orders {
+.orders-hero__heading {
+ position: relative;
display: flex;
flex-direction: column;
- gap: 20px;
- padding: 20px;
+ gap: 6px;
+}
+
+.orders-hero__heading h1 {
+ margin: 0;
+ font-size: 26px;
+ color: #312e81;
+}
+
+.orders-hero__heading p {
+ margin: 0;
+ color: #4338ca;
+ font-size: 14px;
+ opacity: 0.85;
}
-.active-orders {
+.orders-section {
display: flex;
flex-direction: column;
- gap: 20px;
- padding: 20px;
+ gap: 24px;
}
.orders-list {
@@ -954,11 +1001,10 @@ button {
gap: 15px;
}
-.completed-orders {
+.orders-detail-stack {
display: flex;
flex-direction: column;
- gap: 16px;
- padding: 20px;
+ gap: 20px;
}
.completed-orders-list {
@@ -967,6 +1013,28 @@ button {
gap: 12px;
}
+.orders-history {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 20px;
+ background: rgba(255, 255, 255, 0.88);
+ border-radius: 18px;
+ box-shadow: 0 12px 35px rgba(102, 126, 234, 0.18);
+}
+
+.orders-history h2 {
+ margin: 0;
+ font-size: 18px;
+ color: #312e81;
+}
+
+.orders-history__empty {
+ margin: 0;
+ color: #6b7280;
+ font-size: 14px;
+}
+
.completed-order-card {
display: flex;
flex-direction: column;
@@ -1663,16 +1731,15 @@ button {
right: calc(50% - 210px - 20px);
}
- .orders-content {
- flex-direction: row;
- align-items: flex-start;
+ .tabs {
+ top: 24px;
}
- .orders-list {
- flex: 1;
+ .orders-section {
+ gap: 28px;
}
- .order-detail {
- flex: 1;
+ .orders-detail-stack {
+ gap: 24px;
}
}
From 6416f46ed21778615aeb951162f3ea662336c0a9 Mon Sep 17 00:00:00 2001
From: Sahil Kashyap <32007662+sahilkashyap64@users.noreply.github.com>
Date: Fri, 10 Oct 2025 00:43:27 +0530
Subject: [PATCH 6/7] Convert driver portal to JSX modules
---
src/App.jsx | 112 +++++-------------
src/App.tsx | 43 -------
.../{BottomNav.tsx => BottomNav.jsx} | 2 +-
src/components/{Header.tsx => Header.jsx} | 13 +-
src/components/ProtectedRoute.jsx | 19 +--
src/components/ProtectedRoute.tsx | 21 ----
.../{SignaturePad.tsx => SignaturePad.jsx} | 17 +--
src/components/Tabs.jsx | 19 +++
src/components/Tabs.tsx | 29 -----
.../{TimerChip.tsx => TimerChip.jsx} | 11 +-
...{ToastContainer.tsx => ToastContainer.jsx} | 11 +-
...erifyChecklist.tsx => VerifyChecklist.jsx} | 8 +-
src/hooks/{useAuth.tsx => useAuth.jsx} | 74 +++++-------
src/hooks/useOrders.jsx | 97 +++++++++++++++
src/hooks/useOrders.tsx | 107 -----------------
src/hooks/useSocket.jsx | 32 +++++
src/hooks/useSocket.tsx | 38 ------
src/hooks/useToast.jsx | 31 +++++
src/hooks/useToast.tsx | 46 -------
src/main.jsx | 25 ++--
src/main.tsx | 21 ----
src/routes/Chat/{Thread.tsx => Thread.jsx} | 21 ++--
src/routes/{Login.tsx => Login.jsx} | 10 +-
...edOrderCard.tsx => CompletedOrderCard.jsx} | 23 +---
src/routes/Orders/{Index.tsx => Index.jsx} | 39 +++---
.../Orders/{OrderCard.tsx => OrderCard.jsx} | 16 +--
.../{OrderDetail.tsx => OrderDetail.jsx} | 26 ++--
src/routes/{Profile.tsx => Profile.jsx} | 15 +--
28 files changed, 336 insertions(+), 590 deletions(-)
delete mode 100644 src/App.tsx
rename src/components/{BottomNav.tsx => BottomNav.jsx} (94%)
rename src/components/{Header.tsx => Header.jsx} (77%)
delete mode 100644 src/components/ProtectedRoute.tsx
rename src/components/{SignaturePad.tsx => SignaturePad.jsx} (87%)
create mode 100644 src/components/Tabs.jsx
delete mode 100644 src/components/Tabs.tsx
rename src/components/{TimerChip.tsx => TimerChip.jsx} (70%)
rename src/components/{ToastContainer.tsx => ToastContainer.jsx} (64%)
rename src/components/{VerifyChecklist.tsx => VerifyChecklist.jsx} (77%)
rename src/hooks/{useAuth.tsx => useAuth.jsx} (58%)
create mode 100644 src/hooks/useOrders.jsx
delete mode 100644 src/hooks/useOrders.tsx
create mode 100644 src/hooks/useSocket.jsx
delete mode 100644 src/hooks/useSocket.tsx
create mode 100644 src/hooks/useToast.jsx
delete mode 100644 src/hooks/useToast.tsx
delete mode 100644 src/main.tsx
rename src/routes/Chat/{Thread.tsx => Thread.jsx} (78%)
rename src/routes/{Login.tsx => Login.jsx} (91%)
rename src/routes/Orders/{CompletedOrderCard.tsx => CompletedOrderCard.jsx} (73%)
rename src/routes/Orders/{Index.tsx => Index.jsx} (85%)
rename src/routes/Orders/{OrderCard.tsx => OrderCard.jsx} (81%)
rename src/routes/Orders/{OrderDetail.tsx => OrderDetail.jsx} (83%)
rename src/routes/{Profile.tsx => Profile.jsx} (83%)
diff --git a/src/App.jsx b/src/App.jsx
index 38e7dc2..4c07c34 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,80 +1,27 @@
-import { useCallback } from 'react'
-import {
- Link,
- Navigate,
- NavLink,
- Outlet,
- Route,
- Routes,
- useLocation,
- useNavigate,
-} from 'react-router-dom'
+import { Navigate, Outlet, Route, Routes } from 'react-router-dom'
+import LoginRoute from './routes/Login.jsx'
+import OrdersRoute from './routes/Orders/Index.jsx'
+import ProfileRoute from './routes/Profile.jsx'
import ProtectedRoute from './components/ProtectedRoute.jsx'
-import { useAuth } from './context/AuthContext.jsx'
-import LoginPage from './pages/Login.jsx'
-import SettingsPage from './pages/Settings.jsx'
-import OrdersFeed from './pages/orders/OrdersFeed.jsx'
-import OrderDetails from './pages/orders/OrderDetails.jsx'
-import OrderCamera from './pages/orders/OrderCamera.jsx'
-import OrderSignature from './pages/orders/OrderSignature.jsx'
-import OrderBypass from './pages/orders/OrderBypass.jsx'
-import OrderCancel from './pages/orders/OrderCancel.jsx'
-import './App.css'
+import Header from './components/Header.jsx'
+import BottomNav from './components/BottomNav.jsx'
+import { useAuth } from './hooks/useAuth.jsx'
+import { useLocationTracking } from './hooks/useLocationTracking.ts'
+import { OrdersProvider } from './hooks/useOrders.jsx'
-function AppLayout() {
- const { user, logout } = useAuth()
- const navigate = useNavigate()
- const location = useLocation()
-
- const handleLogout = useCallback(() => {
- logout()
- navigate('/login', { replace: true })
- }, [logout, navigate])
-
- const isSettingsRoute = location.pathname.startsWith('/settings')
-
- const navItems = [
- { to: '/orders', label: 'Orders' },
- { to: '/settings', label: 'Settings' },
- ]
+function AppShell() {
+ const { driver } = useAuth()
+ const trackingActive = useLocationTracking({ isActive: driver?.status !== 'OFFLINE' })
return (
-
-
-
-
- Jason's Liquor Drivers
-
-
-
-
- {isSettingsRoute ? (
-
- {user?.name?.first} {user?.name?.last}
- {user?.email}
-
- ) : null}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)
}
@@ -82,19 +29,14 @@ function AppLayout() {
export default function App() {
return (
- } />
- }>
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
+ } />
+ }>
+ }>
+ } />
+ } />
+ } />
+
} />
)
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 0db7ced..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-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 { Header } from './components/Header'
-import { BottomNav } from './components/BottomNav'
-import { useAuth } from './hooks/useAuth'
-import { useLocationTracking } from './hooks/useLocationTracking'
-import { OrdersProvider } from './hooks/useOrders'
-
-function AppShell(): JSX.Element {
- const { driver } = useAuth()
- const trackingActive = useLocationTracking({ isActive: driver?.status !== 'OFFLINE' })
-
- return (
-
-
-
-
-
-
-
-
-
- )
-}
-
-export default function App(): JSX.Element {
- return (
-
- } />
- }>
- }>
- } />
- } />
- } />
-
-
- } />
-
- )
-}
diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.jsx
similarity index 94%
rename from src/components/BottomNav.tsx
rename to src/components/BottomNav.jsx
index b2e1626..e064d4b 100644
--- a/src/components/BottomNav.tsx
+++ b/src/components/BottomNav.jsx
@@ -1,6 +1,6 @@
import { NavLink } from 'react-router-dom'
-export function BottomNav(): JSX.Element {
+export default function BottomNav() {
return (
diff --git a/src/components/VerifyChecklist.tsx b/src/components/VerifyChecklist.jsx
similarity index 77%
rename from src/components/VerifyChecklist.tsx
rename to src/components/VerifyChecklist.jsx
index 335518f..c91c920 100644
--- a/src/components/VerifyChecklist.tsx
+++ b/src/components/VerifyChecklist.jsx
@@ -1,10 +1,4 @@
-interface VerifyChecklistProps {
- idChecked: boolean
- paymentChecked: boolean
- onChange: (next: { idChecked: boolean; paymentChecked: boolean }) => void
-}
-
-export function VerifyChecklist({ idChecked, paymentChecked, onChange }: VerifyChecklistProps): JSX.Element {
+export default function VerifyChecklist({ idChecked, paymentChecked, onChange }) {
return (
diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.jsx
similarity index 58%
rename from src/hooks/useAuth.tsx
rename to src/hooks/useAuth.jsx
index 8a59be3..fc74bb8 100644
--- a/src/hooks/useAuth.tsx
+++ b/src/hooks/useAuth.jsx
@@ -1,33 +1,15 @@
-import {
- PropsWithChildren,
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from 'react'
-import { getCurrentDriver, login as loginApi, updateDriverStatus } from '../api/auth'
-import { apiClient } from '../api/client'
-import { Driver, DriverStatus } from '../types'
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
+import { getCurrentDriver, login as loginApi, updateDriverStatus } from '../api/auth.ts'
+import { apiClient } from '../api/client.ts'
-interface AuthContextValue {
- driver?: Driver
- token?: string
- loading: boolean
- login: (email: string, password: string) => Promise
- logout: () => void
- setStatus: (status: DriverStatus) => Promise
-}
-
-const AuthContext = createContext(undefined)
+const AuthContext = createContext(undefined)
const TOKEN_KEY = 'jason-driver-token'
const DRIVER_KEY = 'jason-driver-profile'
-export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
- const [driver, setDriver] = useState(undefined)
- const [token, setToken] = useState(undefined)
+export function AuthProvider({ children }) {
+ const [driver, setDriver] = useState()
+ const [token, setToken] = useState()
const [loading, setLoading] = useState(true)
useEffect(() => {
@@ -39,7 +21,7 @@ export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
}
if (storedDriver) {
try {
- setDriver(JSON.parse(storedDriver) as Driver)
+ setDriver(JSON.parse(storedDriver))
} catch (error) {
console.warn('Failed to parse stored driver', error)
}
@@ -47,7 +29,7 @@ export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
setLoading(false)
}, [])
- const persist = useCallback((nextDriver: Driver, nextToken?: string) => {
+ const persist = useCallback((nextDriver, nextToken) => {
setDriver(nextDriver)
if (nextToken) {
setToken(nextToken)
@@ -57,15 +39,18 @@ export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
sessionStorage.setItem(DRIVER_KEY, JSON.stringify(nextDriver))
}, [])
- const login = useCallback(async (email: string, password: string) => {
- setLoading(true)
- try {
- const response = await loginApi({ email, password })
- persist(response.driver, response.token)
- } finally {
- setLoading(false)
- }
- }, [persist])
+ const login = useCallback(
+ async (email, password) => {
+ setLoading(true)
+ try {
+ const response = await loginApi({ email, password })
+ persist(response.driver, response.token)
+ } finally {
+ setLoading(false)
+ }
+ },
+ [persist],
+ )
const logout = useCallback(() => {
setDriver(undefined)
@@ -95,13 +80,16 @@ export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
bootstrap()
}, [logout, persist, token])
- const setStatus = useCallback(async (status: DriverStatus) => {
- if (!driver) return
- const updated = await updateDriverStatus(status)
- persist(updated, token)
- }, [driver, persist, token])
+ const setStatus = useCallback(
+ async (status) => {
+ if (!driver) return
+ const updated = await updateDriverStatus(status)
+ persist(updated, token)
+ },
+ [driver, persist, token],
+ )
- const value = useMemo(
+ const value = useMemo(
() => ({ driver, token, loading, login, logout, setStatus }),
[driver, token, loading, login, logout, setStatus],
)
@@ -109,7 +97,7 @@ export function AuthProvider({ children }: PropsWithChildren): JSX.Element {
return {children}
}
-export function useAuth(): AuthContextValue {
+export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
diff --git a/src/hooks/useOrders.jsx b/src/hooks/useOrders.jsx
new file mode 100644
index 0000000..7863c31
--- /dev/null
+++ b/src/hooks/useOrders.jsx
@@ -0,0 +1,97 @@
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
+import { acceptOrder, arriveOrder, completeOrder, getOrders } from '../api/orders.ts'
+import { useSocket } from './useSocket.jsx'
+import { useToast } from './useToast.jsx'
+import { useInterval } from './useInterval.ts'
+
+const OrdersContext = createContext(undefined)
+
+export function OrdersProvider({ children }) {
+ const [orders, setOrders] = useState([])
+ const [isFetching, setIsFetching] = useState(false)
+ const { subscribe } = useSocket()
+ const { push } = useToast()
+
+ const mergeOrder = useCallback((next) => {
+ const normalized = { ...next }
+ setOrders((current) => {
+ const existing = current.find((order) => order.id === normalized.id)
+ if (existing) {
+ return current.map((order) => (order.id === normalized.id ? { ...existing, ...normalized } : order))
+ }
+ return [normalized, ...current]
+ })
+ }, [])
+
+ const refresh = useCallback(async () => {
+ setIsFetching(true)
+ try {
+ const data = await getOrders()
+ setOrders(data.map((order) => ({ ...order })))
+ } finally {
+ setIsFetching(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ refresh()
+ }, [refresh])
+
+ useInterval(refresh, 15000)
+
+ useEffect(() => {
+ const unsubCreated = subscribe('ORDER_CREATED', (payload) => {
+ mergeOrder(payload)
+ push({ title: 'New delivery assigned', description: payload.number, variant: 'info' })
+ })
+ const unsubUpdated = subscribe('ORDER_UPDATED', (payload) => {
+ mergeOrder(payload)
+ })
+ return () => {
+ unsubCreated()
+ unsubUpdated()
+ }
+ }, [mergeOrder, push, subscribe])
+
+ const accept = useCallback(
+ async (orderId) => {
+ const order = await acceptOrder(orderId)
+ mergeOrder(order)
+ push({ title: 'Order accepted', description: order.number, variant: 'success' })
+ },
+ [mergeOrder, push],
+ )
+
+ const markArrived = useCallback(
+ async (orderId) => {
+ const order = await arriveOrder(orderId)
+ mergeOrder(order)
+ push({ title: 'Customer arrival logged', description: order.number, variant: 'info' })
+ },
+ [mergeOrder, push],
+ )
+
+ const markComplete = useCallback(
+ async (orderId, signature) => {
+ const order = await completeOrder(orderId, signature)
+ mergeOrder(order)
+ push({ title: 'Delivery completed', description: order.number, variant: 'success' })
+ },
+ [mergeOrder, push],
+ )
+
+ const value = useMemo(
+ () => ({ orders, refresh, accept, markArrived, markComplete, isFetching }),
+ [accept, isFetching, markArrived, markComplete, orders, refresh],
+ )
+
+ return {children}
+}
+
+export function useOrders() {
+ const context = useContext(OrdersContext)
+ if (!context) {
+ throw new Error('useOrders must be used within OrdersProvider')
+ }
+ return context
+}
diff --git a/src/hooks/useOrders.tsx b/src/hooks/useOrders.tsx
deleted file mode 100644
index 478792e..0000000
--- a/src/hooks/useOrders.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import {
- PropsWithChildren,
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from 'react'
-import { acceptOrder, completeOrder, getOrders } from '../api/orders'
-import { arriveOrder } from '../api/orders'
-import { Order } from '../types'
-import { useSocket } from './useSocket'
-import { useToast } from './useToast'
-import { useInterval } from './useInterval'
-
-interface OrdersContextValue {
- orders: Order[]
- refresh: () => Promise
- accept: (orderId: string) => Promise
- markArrived: (orderId: string) => Promise
- markComplete: (orderId: string, signature?: string) => Promise
- isFetching: boolean
-}
-
-const OrdersContext = createContext(undefined)
-
-export function OrdersProvider({ children }: PropsWithChildren): JSX.Element {
- const [orders, setOrders] = useState([])
- const [isFetching, setIsFetching] = useState(false)
- const { subscribe } = useSocket()
- const { push } = useToast()
-
- const mergeOrder = useCallback((next: Order) => {
- const normalized = { ...next }
- setOrders((current) => {
- const existing = current.find((order) => order.id === normalized.id)
- if (existing) {
- return current.map((order) => (order.id === normalized.id ? { ...existing, ...normalized } : order))
- }
- return [normalized, ...current]
- })
- }, [])
-
- const refresh = useCallback(async () => {
- setIsFetching(true)
- try {
- const data = await getOrders()
- setOrders(data.map((order) => ({ ...order })))
- } finally {
- setIsFetching(false)
- }
- }, [])
-
- useEffect(() => {
- refresh()
- }, [refresh])
-
- useInterval(refresh, 15000)
-
- useEffect(() => {
- const unsubCreated = subscribe('ORDER_CREATED', (payload) => {
- mergeOrder(payload)
- push({ title: 'New delivery assigned', description: payload.number, variant: 'info' })
- })
- const unsubUpdated = subscribe('ORDER_UPDATED', (payload) => {
- mergeOrder(payload)
- })
- return () => {
- unsubCreated()
- unsubUpdated()
- }
- }, [mergeOrder, push, subscribe])
-
- const accept = useCallback(async (orderId: string) => {
- const order = await acceptOrder(orderId)
- mergeOrder(order)
- push({ title: 'Order accepted', description: order.number, variant: 'success' })
- }, [mergeOrder, push])
-
- const markArrived = useCallback(async (orderId: string) => {
- const order = await arriveOrder(orderId)
- mergeOrder(order)
- push({ title: 'Customer arrival logged', description: order.number, variant: 'info' })
- }, [mergeOrder, push])
-
- const markComplete = useCallback(async (orderId: string, signature?: string) => {
- const order = await completeOrder(orderId, signature)
- mergeOrder(order)
- push({ title: 'Delivery completed', description: order.number, variant: 'success' })
- }, [mergeOrder, push])
-
- const value = useMemo(
- () => ({ orders, refresh, accept, markArrived, markComplete, isFetching }),
- [accept, isFetching, markArrived, markComplete, orders, refresh],
- )
-
- return {children}
-}
-
-export function useOrders(): OrdersContextValue {
- const context = useContext(OrdersContext)
- if (!context) {
- throw new Error('useOrders must be used within OrdersProvider')
- }
- return context
-}
diff --git a/src/hooks/useSocket.jsx b/src/hooks/useSocket.jsx
new file mode 100644
index 0000000..0c71e8f
--- /dev/null
+++ b/src/hooks/useSocket.jsx
@@ -0,0 +1,32 @@
+import { createContext, useContext, useEffect, useMemo } from 'react'
+import { createSocket } from '../ws/socket.ts'
+
+const SocketContext = createContext(undefined)
+
+export function SocketProvider({ children }) {
+ const socket = useMemo(() => createSocket(), [])
+
+ useEffect(() => {
+ socket.connect()
+ return () => socket.disconnect()
+ }, [socket])
+
+ const value = useMemo(
+ () => ({
+ socket,
+ subscribe: (type, handler) => socket.on(type, handler),
+ emit: (type, payload) => socket.emit(type, payload),
+ }),
+ [socket],
+ )
+
+ return {children}
+}
+
+export function useSocket() {
+ const context = useContext(SocketContext)
+ if (!context) {
+ throw new Error('useSocket must be used within SocketProvider')
+ }
+ return context
+}
diff --git a/src/hooks/useSocket.tsx b/src/hooks/useSocket.tsx
deleted file mode 100644
index af6ee9a..0000000
--- a/src/hooks/useSocket.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from 'react'
-import { createSocket, SocketClient, SocketHandler } from '../ws/socket'
-
-interface SocketContextValue {
- socket: SocketClient
- subscribe: (type: string, handler: SocketHandler) => () => void
- emit: (type: string, payload: T) => void
-}
-
-const SocketContext = createContext(undefined)
-
-export function SocketProvider({ children }: PropsWithChildren): JSX.Element {
- const socket = useMemo(() => createSocket(), [])
-
- useEffect(() => {
- socket.connect()
- return () => socket.disconnect()
- }, [socket])
-
- const value = useMemo(
- () => ({
- socket,
- subscribe: (type, handler) => socket.on(type, handler),
- emit: (type, payload) => socket.emit(type, payload),
- }),
- [socket],
- )
-
- return {children}
-}
-
-export function useSocket(): SocketContextValue {
- const context = useContext(SocketContext)
- if (!context) {
- throw new Error('useSocket must be used within SocketProvider')
- }
- return context
-}
diff --git a/src/hooks/useToast.jsx b/src/hooks/useToast.jsx
new file mode 100644
index 0000000..07b81f4
--- /dev/null
+++ b/src/hooks/useToast.jsx
@@ -0,0 +1,31 @@
+import { createContext, useCallback, useContext, useMemo, useState } from 'react'
+
+const ToastContext = createContext(undefined)
+
+export function ToastProvider({ children }) {
+ const [toasts, setToasts] = useState([])
+
+ const push = useCallback((toast) => {
+ const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `toast-${Date.now()}`
+ setToasts((current) => [...current, { id, ...toast }])
+ window.setTimeout(() => {
+ setToasts((current) => current.filter((item) => item.id !== id))
+ }, 5000)
+ }, [])
+
+ const dismiss = useCallback((id) => {
+ setToasts((current) => current.filter((item) => item.id !== id))
+ }, [])
+
+ const value = useMemo(() => ({ toasts, push, dismiss }), [dismiss, push, toasts])
+
+ return {children}
+}
+
+export function useToast() {
+ const context = useContext(ToastContext)
+ if (!context) {
+ throw new Error('useToast must be used within ToastProvider')
+ }
+ return context
+}
diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx
deleted file mode 100644
index 471e131..0000000
--- a/src/hooks/useToast.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from 'react'
-
-export type ToastVariant = 'info' | 'success' | 'error'
-
-export interface ToastMessage {
- id: string
- title: string
- description?: string
- variant: ToastVariant
-}
-
-interface ToastContextValue {
- toasts: ToastMessage[]
- push: (toast: Omit) => void
- dismiss: (id: string) => void
-}
-
-const ToastContext = createContext(undefined)
-
-export function ToastProvider({ children }: PropsWithChildren): JSX.Element {
- const [toasts, setToasts] = useState([])
-
- const push = useCallback((toast: Omit) => {
- const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `toast-${Date.now()}`
- setToasts((current) => [...current, { id, ...toast }])
- window.setTimeout(() => {
- setToasts((current) => current.filter((item) => item.id !== id))
- }, 5000)
- }, [])
-
- const dismiss = useCallback((id: string) => {
- setToasts((current) => current.filter((item) => item.id !== id))
- }, [])
-
- const value = useMemo(() => ({ toasts, push, dismiss }), [dismiss, push, toasts])
-
- return {children}
-}
-
-export function useToast(): ToastContextValue {
- const context = useContext(ToastContext)
- if (!context) {
- throw new Error('useToast must be used within ToastProvider')
- }
- return context
-}
diff --git a/src/main.jsx b/src/main.jsx
index 2af23dc..e55a62a 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,16 +1,21 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
+import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
-import './styles/globals.css'
import App from './App.jsx'
-import { AuthProvider } from './context/AuthContext.jsx'
+import './styles/globals.css'
+import { AuthProvider } from './hooks/useAuth.jsx'
+import { ToastProvider } from './hooks/useToast.jsx'
+import { SocketProvider } from './hooks/useSocket.jsx'
+import ToastContainer from './components/ToastContainer.jsx'
-createRoot(document.getElementById('root')).render(
-
-
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
-
+
+
+
-
- ,
+
+
+ ,
)
diff --git a/src/main.tsx b/src/main.tsx
deleted file mode 100644
index ac2a5f1..0000000
--- a/src/main.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import ReactDOM from 'react-dom/client'
-import { BrowserRouter } from 'react-router-dom'
-import App from './App'
-import './styles/globals.css'
-import { AuthProvider } from './hooks/useAuth'
-import { ToastProvider } from './hooks/useToast'
-import { SocketProvider } from './hooks/useSocket'
-import { ToastContainer } from './components/ToastContainer'
-
-ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
-
-
-
-
-
-
-
-
- ,
-)
diff --git a/src/routes/Chat/Thread.tsx b/src/routes/Chat/Thread.jsx
similarity index 78%
rename from src/routes/Chat/Thread.tsx
rename to src/routes/Chat/Thread.jsx
index 53935ed..195883f 100644
--- a/src/routes/Chat/Thread.tsx
+++ b/src/routes/Chat/Thread.jsx
@@ -1,13 +1,12 @@
-import { FormEvent, useEffect, useRef, useState } from 'react'
-import { getMessages, sendMessage } from '../../api/chat'
-import { Message } from '../../types'
-import { useSocket } from '../../hooks/useSocket'
-import { useToast } from '../../hooks/useToast'
+import { useEffect, useRef, useState } from 'react'
+import { getMessages, sendMessage } from '../../api/chat.ts'
+import { useSocket } from '../../hooks/useSocket.jsx'
+import { useToast } from '../../hooks/useToast.jsx'
-export default function ChatThread(): JSX.Element {
- const [messages, setMessages] = useState([])
+export default function ChatThread() {
+ const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
- const listRef = useRef(null)
+ const listRef = useRef(null)
const { subscribe, emit } = useSocket()
const { push } = useToast()
@@ -21,7 +20,7 @@ export default function ChatThread(): JSX.Element {
}, [])
useEffect(() => {
- const unsubscribe = subscribe('CHAT_MESSAGE', (payload) => {
+ const unsubscribe = subscribe('CHAT_MESSAGE', (payload) => {
setMessages((current) => {
if (current.some((message) => message.id === payload.id)) {
return current
@@ -41,10 +40,10 @@ export default function ChatThread(): JSX.Element {
})
}
- async function handleSubmit(event: FormEvent) {
+ async function handleSubmit(event) {
event.preventDefault()
if (!input.trim()) return
- const optimistic: Message = {
+ const optimistic = {
id: `optimistic-${Date.now()}`,
sender: 'DRIVER',
text: input,
diff --git a/src/routes/Login.tsx b/src/routes/Login.jsx
similarity index 91%
rename from src/routes/Login.tsx
rename to src/routes/Login.jsx
index 4a3a98e..5c45893 100644
--- a/src/routes/Login.tsx
+++ b/src/routes/Login.jsx
@@ -1,9 +1,9 @@
-import { FormEvent, useState } from 'react'
+import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
-import { useAuth } from '../hooks/useAuth'
-import { useToast } from '../hooks/useToast'
+import { useAuth } from '../hooks/useAuth.jsx'
+import { useToast } from '../hooks/useToast.jsx'
-export default function LoginRoute(): JSX.Element {
+export default function LoginRoute() {
const { login } = useAuth()
const navigate = useNavigate()
const { push } = useToast()
@@ -11,7 +11,7 @@ export default function LoginRoute(): JSX.Element {
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
- async function handleSubmit(event: FormEvent) {
+ async function handleSubmit(event) {
event.preventDefault()
setLoading(true)
try {
diff --git a/src/routes/Orders/CompletedOrderCard.tsx b/src/routes/Orders/CompletedOrderCard.jsx
similarity index 73%
rename from src/routes/Orders/CompletedOrderCard.tsx
rename to src/routes/Orders/CompletedOrderCard.jsx
index 755711b..2d4dcda 100644
--- a/src/routes/Orders/CompletedOrderCard.tsx
+++ b/src/routes/Orders/CompletedOrderCard.jsx
@@ -1,23 +1,8 @@
-import { Order } from '../../types'
-import { OrderDetail } from './OrderDetail'
-import { classNames } from '../../utils/classNames'
-import { formatTimeOfDay, getInitials } from '../../utils/format'
+import OrderDetail from './OrderDetail.jsx'
+import { classNames } from '../../utils/classNames.ts'
+import { formatTimeOfDay, getInitials } from '../../utils/format.ts'
-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 {
+export default function CompletedOrderCard({ order, expanded, onToggle, onArrive, onComplete }) {
const deliveredTime = formatTimeOfDay(order.createdAt)
return (
diff --git a/src/routes/Orders/Index.tsx b/src/routes/Orders/Index.jsx
similarity index 85%
rename from src/routes/Orders/Index.tsx
rename to src/routes/Orders/Index.jsx
index 4034baa..690256c 100644
--- a/src/routes/Orders/Index.tsx
+++ b/src/routes/Orders/Index.jsx
@@ -1,12 +1,11 @@
import { useEffect, useMemo, useState } from 'react'
-import { useOrders } from '../../hooks/useOrders'
-import { Tabs } from '../../components/Tabs'
-import { OrderCard } from './OrderCard'
-import { OrderDetail } from './OrderDetail'
-import { CompletedOrderCard } from './CompletedOrderCard'
-import { Order } from '../../types'
-import { useToast } from '../../hooks/useToast'
-import { useAuth } from '../../hooks/useAuth'
+import { useOrders } from '../../hooks/useOrders.jsx'
+import Tabs from '../../components/Tabs.jsx'
+import OrderCard from './OrderCard.jsx'
+import OrderDetail from './OrderDetail.jsx'
+import CompletedOrderCard from './CompletedOrderCard.jsx'
+import { useToast } from '../../hooks/useToast.jsx'
+import { useAuth } from '../../hooks/useAuth.jsx'
const tabConfig = [
{ id: 'assigned', label: 'Assigned' },
@@ -14,14 +13,12 @@ const tabConfig = [
{ id: 'out-for-delivery', label: 'Out for delivery' },
]
-type TabId = (typeof tabConfig)[number]['id']
-
-export default function OrdersRoute(): JSX.Element {
+export default function OrdersRoute() {
const { orders, accept, markArrived, markComplete } = useOrders()
const { push } = useToast()
const { driver } = useAuth()
- const [activeTab, setActiveTab] = useState('assigned')
- const [expandedCompletedId, setExpandedCompletedId] = useState(undefined)
+ const [activeTab, setActiveTab] = useState('assigned')
+ const [expandedCompletedId, setExpandedCompletedId] = useState()
const [completedVisibleCount, setCompletedVisibleCount] = useState(10)
const segmented = useMemo(() => {
@@ -50,9 +47,7 @@ export default function OrdersRoute(): JSX.Element {
if (!driver) return true
return order.assignedDriverId === driver.id
})
- .sort(
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
- )
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return { assigned, accepted, outForDelivery, completed }
}, [driver, orders])
@@ -63,17 +58,17 @@ export default function OrdersRoute(): JSX.Element {
const canShowMoreCompleted = segmented.completed.length > completedVisibleCount
- const handleAccept = async (order: Order) => {
+ const handleAccept = async (order) => {
await accept(order.id)
setActiveTab('accepted')
}
- const handleArrive = async (orderId: string) => {
+ const handleArrive = async (orderId) => {
await markArrived(orderId)
setActiveTab('out-for-delivery')
}
- const handleComplete = async (orderId: string, signature: string) => {
+ const handleComplete = async (orderId, signature) => {
await markComplete(orderId, signature)
push({ title: 'Signature captured', description: 'Delivery is complete.', variant: 'success' })
}
@@ -123,7 +118,7 @@ export default function OrdersRoute(): JSX.Element {
: undefined,
}))}
activeId={activeTab}
- onChange={(id) => setActiveTab(id as TabId)}
+ onChange={(id) => setActiveTab(id)}
/>
{activeTab === 'assigned' ? (
@@ -183,9 +178,7 @@ 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.jsx
similarity index 81%
rename from src/routes/Orders/OrderCard.tsx
rename to src/routes/Orders/OrderCard.jsx
index 6811b35..6a37dcc 100644
--- a/src/routes/Orders/OrderCard.tsx
+++ b/src/routes/Orders/OrderCard.jsx
@@ -1,16 +1,8 @@
-import { Order } from '../../types'
-import { formatCurrency, getInitials } from '../../utils/format'
-import { TimerChip } from '../../components/TimerChip'
-import { classNames } from '../../utils/classNames'
+import { formatCurrency, getInitials } from '../../utils/format.ts'
+import TimerChip from '../../components/TimerChip.jsx'
+import { classNames } from '../../utils/classNames.ts'
-interface OrderCardProps {
- order: Order
- onAccept?: (order: Order) => void
- onSelect?: (order: Order) => void
- isSelected?: boolean
-}
-
-export function OrderCard({ order, onAccept, onSelect, isSelected }: OrderCardProps): JSX.Element {
+export default function OrderCard({ order, onAccept, onSelect, isSelected }) {
const isPending = order.status === 'NEW'
const statusLabel = order.status === 'COMPLETED' ? 'Completed' : undefined
const isSelectable = Boolean(onSelect)
diff --git a/src/routes/Orders/OrderDetail.tsx b/src/routes/Orders/OrderDetail.jsx
similarity index 83%
rename from src/routes/Orders/OrderDetail.tsx
rename to src/routes/Orders/OrderDetail.jsx
index 72bbf7e..dcf3434 100644
--- a/src/routes/Orders/OrderDetail.tsx
+++ b/src/routes/Orders/OrderDetail.jsx
@@ -1,20 +1,13 @@
import { useEffect, useMemo, useState } from 'react'
-import { Order } from '../../types'
-import { formatCurrency, getInitials } from '../../utils/format'
-import { TimerChip } from '../../components/TimerChip'
-import { VerifyChecklist } from '../../components/VerifyChecklist'
-import { SignaturePad } from '../../components/SignaturePad'
+import { formatCurrency, getInitials } from '../../utils/format.ts'
+import TimerChip from '../../components/TimerChip.jsx'
+import VerifyChecklist from '../../components/VerifyChecklist.jsx'
+import SignaturePad from '../../components/SignaturePad.jsx'
-interface OrderDetailProps {
- order: Order
- onArrive: (orderId: string) => Promise
- onComplete: (orderId: string, signature: string) => Promise
-}
-
-export function OrderDetail({ order, onArrive, onComplete }: OrderDetailProps): JSX.Element {
+export default function OrderDetail({ order, onArrive, onComplete }) {
const [idChecked, setIdChecked] = useState(false)
const [paymentChecked, setPaymentChecked] = useState(false)
- const [signature, setSignature] = useState(null)
+ const [signature, setSignature] = useState(null)
const [submitting, setSubmitting] = useState(false)
useEffect(() => {
@@ -99,12 +92,7 @@ export function OrderDetail({ order, onArrive, onComplete }: OrderDetailProps):
) : null}
{showArriveButton ? (
-
+
I've Arrived at Customer Location →
) : null}
diff --git a/src/routes/Profile.tsx b/src/routes/Profile.jsx
similarity index 83%
rename from src/routes/Profile.tsx
rename to src/routes/Profile.jsx
index 3b80c06..1e3a62f 100644
--- a/src/routes/Profile.tsx
+++ b/src/routes/Profile.jsx
@@ -1,20 +1,15 @@
import { useState } from 'react'
import { useOutletContext } from 'react-router-dom'
-import { useAuth } from '../hooks/useAuth'
-import { useToast } from '../hooks/useToast'
-import { DriverStatus } from '../types'
+import { useAuth } from '../hooks/useAuth.jsx'
+import { useToast } from '../hooks/useToast.jsx'
-interface AppShellContext {
- trackingActive: boolean
-}
-
-export default function ProfileRoute(): JSX.Element {
- const { trackingActive } = useOutletContext()
+export default function ProfileRoute() {
+ const { trackingActive } = useOutletContext()
const { driver, logout, setStatus } = useAuth()
const { push } = useToast()
const [updating, setUpdating] = useState(false)
- async function handleStatusChange(nextStatus: DriverStatus) {
+ async function handleStatusChange(nextStatus) {
if (!driver || driver.status === nextStatus) return
setUpdating(true)
try {
From 5bfa787796f2d265b7951100922e3b9090150f01 Mon Sep 17 00:00:00 2001
From: Sahil Kashyap <32007662+sahilkashyap64@users.noreply.github.com>
Date: Fri, 10 Oct 2025 01:19:05 +0530
Subject: [PATCH 7/7] Replace order mocks with live API integration
---
src/api/mockData.ts | 67 +------
src/api/orders.ts | 77 ---------
src/components/TimerChip.jsx | 16 +-
src/hooks/useOrders.jsx | 279 ++++++++++++++++++++++++++++--
src/routes/Orders/Index.jsx | 34 ++--
src/routes/Orders/OrderCard.jsx | 4 +-
src/routes/Orders/OrderDetail.jsx | 9 +-
src/ws/socket.ts | 10 +-
8 files changed, 308 insertions(+), 188 deletions(-)
delete mode 100644 src/api/orders.ts
diff --git a/src/api/mockData.ts b/src/api/mockData.ts
index e381c8e..1dd799a 100644
--- a/src/api/mockData.ts
+++ b/src/api/mockData.ts
@@ -1,4 +1,4 @@
-import { AuthResponse, Driver, Message, Order } from '../types'
+import { AuthResponse, Driver, Message } from '../types'
const now = new Date()
@@ -14,71 +14,6 @@ export const mockDriver: Driver = {
status: 'ONLINE',
}
-export const mockOrders: Order[] = [
- {
- id: 'order-1',
- number: 'JL-2847',
- total: 127.5,
- status: 'NEW',
- requiresIdCheck: true,
- requiresPaymentCheck: true,
- createdAt: minutesAgo(35),
- customer: {
- name: 'Michael Rodriguez',
- phone: '(555) 123-4567',
- address: '85 Dolores Street, San Francisco, CA 94110',
- },
- priority: true,
- assignedDriverId: 'driver-1',
- items: [
- { id: 'item-1', name: 'Johnnie Walker Black Label', quantity: 1 },
- { id: 'item-2', name: 'Grey Goose Vodka', quantity: 1 },
- { id: 'item-3', name: 'Corona Extra 6-pack', quantity: 1 },
- ],
- },
- {
- id: 'order-2',
- number: 'JL-2846',
- total: 156,
- status: 'IN_PROGRESS',
- requiresIdCheck: true,
- requiresPaymentCheck: true,
- createdAt: minutesAgo(28),
- customer: {
- name: 'John Doe',
- phone: '(555) 246-8135',
- address: '123 Market Street, San Francisco, CA 94103',
- lat: 37.7937,
- lng: -122.396,
- },
- assignedDriverId: 'driver-1',
- items: [
- { id: 'item-4', name: 'Don Julio Blanco', quantity: 1 },
- { id: 'item-5', name: 'Casamigos Reposado', quantity: 1 },
- { id: 'item-6', name: 'Limes', quantity: 6 },
- ],
- },
- {
- id: 'order-3',
- number: 'JL-2845',
- total: 98.4,
- status: 'COMPLETED',
- requiresIdCheck: true,
- requiresPaymentCheck: true,
- createdAt: minutesAgo(125),
- customer: {
- name: 'Alice Lee',
- phone: '(555) 678-9012',
- address: '678 Mission Street, San Francisco, CA 94105',
- },
- assignedDriverId: 'driver-1',
- items: [
- { id: 'item-7', name: 'Veuve Clicquot Brut', quantity: 2 },
- { id: 'item-8', name: 'San Pellegrino', quantity: 4 },
- ],
- },
-]
-
export const mockAuthResponse: AuthResponse = {
token: 'demo-token',
driver: mockDriver,
diff --git a/src/api/orders.ts b/src/api/orders.ts
deleted file mode 100644
index 6766aca..0000000
--- a/src/api/orders.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { apiClient, safeRequest } from './client'
-import { mockOrders } from './mockData'
-import { Order } from '../types'
-
-export async function getOrders(): Promise {
- return safeRequest(
- async () => {
- const response = await apiClient.get('/orders')
- return response.data
- },
- async () => mockOrders,
- )
-}
-
-export async function acceptOrder(orderId: string): Promise {
- return safeRequest(
- async () => {
- const response = await apiClient.post(`/orders/${orderId}/accept`)
- return response.data
- },
- async () => {
- const existing = mockOrders.find((order) => order.id === orderId)
- if (!existing) {
- throw new Error('Order not found')
- }
- existing.status = 'IN_PROGRESS'
- return existing
- },
- )
-}
-
-export async function arriveOrder(orderId: string): Promise {
- return safeRequest(
- async () => {
- const response = await apiClient.post(`/orders/${orderId}/arrive`)
- return response.data
- },
- async () => {
- const existing = mockOrders.find((order) => order.id === orderId)
- if (!existing) {
- throw new Error('Order not found')
- }
- existing.status = 'ARRIVED'
- return existing
- },
- )
-}
-
-export async function completeOrder(orderId: string, signature?: string): Promise {
- return safeRequest(
- async () => {
- const response = await apiClient.post(`/orders/${orderId}/complete`, { signature })
- return response.data
- },
- async () => {
- const existing = mockOrders.find((order) => order.id === orderId)
- if (!existing) {
- throw new Error('Order not found')
- }
- existing.status = 'COMPLETED'
- return existing
- },
- )
-}
-
-export function getElapsedMinutes(order: Order): number {
- const start = new Date(order.createdAt)
- const now = new Date()
- const diff = (now.getTime() - start.getTime()) / 60000
- return Math.max(0, diff)
-}
-
-export function getEta(order: Order): string {
- const minutes = getElapsedMinutes(order)
- const eta = new Date(new Date(order.createdAt).getTime() + Math.round(minutes) * 60000)
- return eta.toISOString()
-}
diff --git a/src/components/TimerChip.jsx b/src/components/TimerChip.jsx
index c13e504..fdc6b5e 100644
--- a/src/components/TimerChip.jsx
+++ b/src/components/TimerChip.jsx
@@ -1,4 +1,18 @@
-import { getElapsedMinutes } from '../api/orders.ts'
+function getElapsedMinutes(order) {
+ if (!order) {
+ return 0
+ }
+ const timestamp = order.createdAt ?? order.created_at ?? order.updatedAt
+ if (!timestamp) {
+ return 0
+ }
+ const start = new Date(timestamp)
+ if (Number.isNaN(start.getTime())) {
+ return 0
+ }
+ const diff = (Date.now() - start.getTime()) / 60000
+ return diff > 0 ? diff : 0
+}
function formatDuration(minutes) {
const wholeMinutes = Math.floor(minutes)
diff --git a/src/hooks/useOrders.jsx b/src/hooks/useOrders.jsx
index 7863c31..8762b7d 100644
--- a/src/hooks/useOrders.jsx
+++ b/src/hooks/useOrders.jsx
@@ -1,37 +1,211 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
-import { acceptOrder, arriveOrder, completeOrder, getOrders } from '../api/orders.ts'
+import { fetchOrders, updateOrder, updateOrderStatus, uploadImage } from '../services/orderService'
import { useSocket } from './useSocket.jsx'
import { useToast } from './useToast.jsx'
import { useInterval } from './useInterval.ts'
+import { useAuth } from './useAuth.jsx'
const OrdersContext = createContext(undefined)
+function resolveStage(status) {
+ const normalized = (status ?? '').toString().trim().toLowerCase()
+ if (['accepted', 'acknowledged'].includes(normalized)) {
+ return 'accepted'
+ }
+ if (
+ ['in progress', 'out for delivery', 'out-for-delivery', 'delivering', 'arrived'].includes(
+ normalized,
+ )
+ ) {
+ return 'out-for-delivery'
+ }
+ if (['completed', 'delivered'].includes(normalized)) {
+ return 'completed'
+ }
+ return 'assigned'
+}
+
+function coerceDate(value) {
+ if (!value) {
+ return new Date().toISOString()
+ }
+ const parsed = new Date(value)
+ if (Number.isNaN(parsed.getTime())) {
+ return new Date().toISOString()
+ }
+ return parsed.toISOString()
+}
+
+function formatAddress(...sources) {
+ for (const source of sources) {
+ if (!source) continue
+ if (typeof source === 'string') {
+ const trimmed = source.trim()
+ if (trimmed) {
+ return trimmed
+ }
+ continue
+ }
+ if (typeof source === 'object') {
+ const apartment =
+ source.apartment ?? source.apartmentNumber ?? source.unit ?? source.suite ?? source.flat
+ const description =
+ source.description ??
+ source.line1 ??
+ source.street ??
+ source.address1 ??
+ source.address ??
+ source.formatted ??
+ source.formattedAddress
+ const line2 = source.line2 ?? source.street2 ?? source.address2 ?? ''
+ const city = source.city ?? source.town ?? source.locality ?? ''
+ const state = source.state ?? source.region ?? ''
+ const postal = source.zip ?? source.postalCode ?? source.postcode ?? ''
+ const parts = [
+ apartment ? `Apt ${apartment}` : null,
+ description,
+ line2,
+ [city, state, postal].filter(Boolean).join(', '),
+ ]
+ .filter(Boolean)
+ .map((part) => part.trim())
+ .filter(Boolean)
+
+ if (parts.length > 0) {
+ return parts.join(', ')
+ }
+ }
+ }
+ return 'Address unavailable'
+}
+
+function extractItems(order) {
+ if (!order) return []
+ if (Array.isArray(order.items) && order.items.length > 0) {
+ return order.items.map((item, index) => ({
+ id: String(item._id ?? item.id ?? index),
+ name:
+ item.name ??
+ item.Description ??
+ item.title ??
+ item.productName ??
+ `Item ${index + 1}`,
+ quantity: Number(item.quantity ?? item.qty ?? item.count ?? 1) || 1,
+ }))
+ }
+
+ const products = Array.isArray(order.products) ? order.products : []
+ const quantities = Array.isArray(order.qty) ? order.qty : []
+ return products.map((product, index) => ({
+ id: String(product._id ?? product.id ?? index),
+ name: product.Description ?? product.name ?? product.title ?? `Item ${index + 1}`,
+ quantity: Number(quantities[index]) || Number(product.quantity ?? 1) || 1,
+ }))
+}
+
+function normalizeOrder(order) {
+ if (!order || typeof order !== 'object') {
+ return null
+ }
+ const idCandidate =
+ order._id ??
+ order.id ??
+ order.orderId ??
+ order.orderID ??
+ order.reference ??
+ order.number ??
+ order.uuid
+ if (!idCandidate) {
+ return null
+ }
+ const id = String(idCandidate)
+ const rawStatus = order.status ?? order.orderStatus ?? order.currentStatus ?? ''
+ const stage = resolveStage(rawStatus)
+ const owner = order.owner ?? order.customer ?? {}
+ const firstName =
+ owner?.name?.first ?? owner?.firstName ?? owner?.firstname ?? owner?.first ?? ''
+ const lastName = owner?.name?.last ?? owner?.lastName ?? owner?.lastname ?? owner?.last ?? ''
+ const combinedName = [firstName, lastName].filter(Boolean).join(' ')
+ const fallbackName =
+ owner?.name?.full ?? owner?.fullName ?? owner?.name ?? owner?.displayName ?? combinedName
+ const name = (fallbackName || 'Customer').trim()
+ const phone =
+ owner?.phone ?? owner?.phoneNumber ?? order.phone ?? order.customerPhone ?? order.contact ?? ''
+ const address = formatAddress(order.address, order.deliveryAddress, order.shippingAddress)
+ const number =
+ order.orderId ??
+ order.number ??
+ order.reference ??
+ order.shortId ??
+ (typeof id === 'string' && id.length >= 6 ? id.slice(-6).toUpperCase() : id)
+ const total = Number(
+ order.total ?? order.totalAmount ?? order.orderTotal ?? order.amount ?? order.paymentTotal ?? 0,
+ )
+ const createdAt = coerceDate(order.createdAt ?? order.created_at ?? order.updatedAt)
+ const assignedDriverId =
+ order.assignedDriverId ??
+ order.driverId ??
+ order.driver_id ??
+ order.driver?.id ??
+ order.driver?._id ??
+ undefined
+
+ return {
+ id,
+ number: String(number),
+ total: Number.isFinite(total) ? total : 0,
+ status: stage,
+ rawStatus: rawStatus ?? '',
+ createdAt,
+ customer: {
+ name,
+ phone: phone ?? '',
+ address,
+ },
+ priority: Boolean(order.priority || order.isPriority || order.priorityOrder),
+ items: extractItems(order),
+ assignedDriverId,
+ }
+}
+
export function OrdersProvider({ children }) {
const [orders, setOrders] = useState([])
const [isFetching, setIsFetching] = useState(false)
const { subscribe } = useSocket()
const { push } = useToast()
+ const { token } = useAuth()
const mergeOrder = useCallback((next) => {
- const normalized = { ...next }
+ const normalized = normalizeOrder(next)
+ if (!normalized) {
+ return
+ }
setOrders((current) => {
const existing = current.find((order) => order.id === normalized.id)
if (existing) {
- return current.map((order) => (order.id === normalized.id ? { ...existing, ...normalized } : order))
+ return current.map((order) =>
+ order.id === normalized.id ? { ...existing, ...normalized } : order,
+ )
}
return [normalized, ...current]
})
}, [])
const refresh = useCallback(async () => {
+ if (!token) {
+ setOrders([])
+ return
+ }
setIsFetching(true)
try {
- const data = await getOrders()
- setOrders(data.map((order) => ({ ...order })))
+ const data = await fetchOrders(token)
+ setOrders(data.map((order) => normalizeOrder(order)).filter(Boolean))
+ } catch (error) {
+ console.error('Failed to load orders', error)
} finally {
setIsFetching(false)
}
- }, [])
+ }, [token])
useEffect(() => {
refresh()
@@ -42,7 +216,10 @@ export function OrdersProvider({ children }) {
useEffect(() => {
const unsubCreated = subscribe('ORDER_CREATED', (payload) => {
mergeOrder(payload)
- push({ title: 'New delivery assigned', description: payload.number, variant: 'info' })
+ const normalized = normalizeOrder(payload)
+ if (normalized?.number) {
+ push({ title: 'New delivery assigned', description: normalized.number, variant: 'info' })
+ }
})
const unsubUpdated = subscribe('ORDER_UPDATED', (payload) => {
mergeOrder(payload)
@@ -55,29 +232,93 @@ export function OrdersProvider({ children }) {
const accept = useCallback(
async (orderId) => {
- const order = await acceptOrder(orderId)
- mergeOrder(order)
- push({ title: 'Order accepted', description: order.number, variant: 'success' })
+ if (!token) {
+ push({
+ title: 'Unable to accept order',
+ description: 'Authentication required.',
+ variant: 'error',
+ })
+ return false
+ }
+ try {
+ await updateOrderStatus(orderId, 'Accepted', token)
+ push({ title: 'Order accepted', description: 'Order moved to Accepted.', variant: 'success' })
+ await refresh()
+ return true
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unable to accept order.'
+ push({ title: 'Unable to accept order', description: message, variant: 'error' })
+ return false
+ }
},
- [mergeOrder, push],
+ [push, refresh, token],
)
const markArrived = useCallback(
async (orderId) => {
- const order = await arriveOrder(orderId)
- mergeOrder(order)
- push({ title: 'Customer arrival logged', description: order.number, variant: 'info' })
+ if (!token) {
+ push({
+ title: 'Unable to update order',
+ description: 'Authentication required.',
+ variant: 'error',
+ })
+ return false
+ }
+ try {
+ await updateOrderStatus(orderId, 'In Progress', token)
+ push({
+ title: 'Customer arrival logged',
+ description: 'Order is now out for delivery.',
+ variant: 'info',
+ })
+ await refresh()
+ return true
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unable to update order.'
+ push({ title: 'Unable to update order', description: message, variant: 'error' })
+ return false
+ }
},
- [mergeOrder, push],
+ [push, refresh, token],
)
const markComplete = useCallback(
async (orderId, signature) => {
- const order = await completeOrder(orderId, signature)
- mergeOrder(order)
- push({ title: 'Delivery completed', description: order.number, variant: 'success' })
+ if (!token) {
+ push({
+ title: 'Unable to complete order',
+ description: 'Authentication required.',
+ variant: 'error',
+ })
+ return false
+ }
+ try {
+ let signatureUrl = signature ?? undefined
+ if (signature && typeof signature === 'string' && signature.startsWith('data:')) {
+ signatureUrl = await uploadImage(signature)
+ }
+ const payload = {
+ _id: orderId,
+ status: 'Completed',
+ }
+ if (signatureUrl) {
+ payload.signature = signatureUrl
+ }
+ await updateOrder(payload, token)
+ push({
+ title: 'Delivery completed',
+ description: 'Order marked as delivered.',
+ variant: 'success',
+ })
+ await refresh()
+ return true
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unable to complete order.'
+ push({ title: 'Unable to complete order', description: message, variant: 'error' })
+ return false
+ }
},
- [mergeOrder, push],
+ [push, refresh, token],
)
const value = useMemo(
diff --git a/src/routes/Orders/Index.jsx b/src/routes/Orders/Index.jsx
index 690256c..3967f31 100644
--- a/src/routes/Orders/Index.jsx
+++ b/src/routes/Orders/Index.jsx
@@ -22,30 +22,30 @@ export default function OrdersRoute() {
const [completedVisibleCount, setCompletedVisibleCount] = useState(10)
const segmented = useMemo(() => {
- const assigned = orders.filter((order) => order.status === 'NEW' || order.status === 'ASSIGNED')
+ const assigned = orders.filter((order) => order.status === 'assigned')
const accepted = orders.filter((order) => {
- if (order.status !== 'IN_PROGRESS') {
+ if (order.status !== 'accepted') {
return false
}
if (!driver) return true
- return order.assignedDriverId === driver.id
+ return order.assignedDriverId ? order.assignedDriverId === driver.id : true
})
const outForDelivery = orders.filter((order) => {
- if (order.status !== 'ARRIVED') {
+ if (order.status !== 'out-for-delivery') {
return false
}
if (!driver) return true
- return order.assignedDriverId === driver.id
+ return order.assignedDriverId ? order.assignedDriverId === driver.id : true
})
const completed = orders
.filter((order) => {
- if (order.status !== 'COMPLETED') return false
+ if (order.status !== 'completed') return false
if (!driver) return true
- return order.assignedDriverId === driver.id
+ return order.assignedDriverId ? order.assignedDriverId === driver.id : true
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return { assigned, accepted, outForDelivery, completed }
@@ -59,18 +59,24 @@ export default function OrdersRoute() {
const canShowMoreCompleted = segmented.completed.length > completedVisibleCount
const handleAccept = async (order) => {
- await accept(order.id)
- setActiveTab('accepted')
+ const success = await accept(order.id)
+ if (success) {
+ setActiveTab('accepted')
+ }
}
const handleArrive = async (orderId) => {
- await markArrived(orderId)
- setActiveTab('out-for-delivery')
+ const success = await markArrived(orderId)
+ if (success) {
+ setActiveTab('out-for-delivery')
+ }
}
const handleComplete = async (orderId, signature) => {
- await markComplete(orderId, signature)
- push({ title: 'Signature captured', description: 'Delivery is complete.', variant: 'success' })
+ const success = await markComplete(orderId, signature)
+ if (success) {
+ push({ title: 'Signature captured', description: 'Delivery is complete.', variant: 'success' })
+ }
}
useEffect(() => {
@@ -115,6 +121,8 @@ export default function OrdersRoute() {
? assignedOrders.length
: tab.id === 'accepted'
? acceptedOrders.length
+ : tab.id === 'out-for-delivery'
+ ? outForDeliveryOrders.length
: undefined,
}))}
activeId={activeTab}
diff --git a/src/routes/Orders/OrderCard.jsx b/src/routes/Orders/OrderCard.jsx
index 6a37dcc..cea1510 100644
--- a/src/routes/Orders/OrderCard.jsx
+++ b/src/routes/Orders/OrderCard.jsx
@@ -3,8 +3,8 @@ import TimerChip from '../../components/TimerChip.jsx'
import { classNames } from '../../utils/classNames.ts'
export default function OrderCard({ order, onAccept, onSelect, isSelected }) {
- const isPending = order.status === 'NEW'
- const statusLabel = order.status === 'COMPLETED' ? 'Completed' : undefined
+ const isPending = order.status === 'assigned'
+ const statusLabel = order.status === 'completed' ? 'Completed' : undefined
const isSelectable = Boolean(onSelect)
return (
diff --git a/src/routes/Orders/OrderDetail.jsx b/src/routes/Orders/OrderDetail.jsx
index dcf3434..9680dde 100644
--- a/src/routes/Orders/OrderDetail.jsx
+++ b/src/routes/Orders/OrderDetail.jsx
@@ -9,17 +9,18 @@ export default function OrderDetail({ order, onArrive, onComplete }) {
const [paymentChecked, setPaymentChecked] = useState(false)
const [signature, setSignature] = useState(null)
const [submitting, setSubmitting] = useState(false)
+ const stage = order.status ?? 'assigned'
useEffect(() => {
setIdChecked(false)
setPaymentChecked(false)
setSignature(null)
setSubmitting(false)
- }, [order.id, order.status])
+ }, [order.id, stage])
const canComplete = idChecked && paymentChecked && Boolean(signature)
- const showVerification = order.status === 'ARRIVED'
- const showArriveButton = order.status === 'IN_PROGRESS'
+ const showVerification = stage === 'out-for-delivery'
+ const showArriveButton = stage === 'accepted'
const mapsQuery = useMemo(() => encodeURIComponent(order.customer.address), [order.customer.address])
@@ -119,7 +120,7 @@ export default function OrderDetail({ order, onArrive, onComplete }) {
Complete Delivery
- ) : order.status === 'COMPLETED' ? (
+ ) : stage === 'completed' ? (
Delivery completed · Signature on file.
) : null}
diff --git a/src/ws/socket.ts b/src/ws/socket.ts
index 070a239..7f5a09b 100644
--- a/src/ws/socket.ts
+++ b/src/ws/socket.ts
@@ -1,4 +1,4 @@
-import { mockMessages, mockOrders } from '../api/mockData'
+import { mockMessages } from '../api/mockData'
import { SocketEvent } from '../types'
export type SocketHandler
= (payload: T) => void
@@ -82,12 +82,10 @@ export class MockSocket extends SocketClient {
connect(): void {
this.timer = window.setInterval(() => {
- const newest = mockOrders.find((order) => order.status === 'NEW')
- if (newest) {
- this.dispatch('ORDER_UPDATED', newest)
- }
const lastMessage = mockMessages[mockMessages.length - 1]
- this.dispatch('CHAT_MESSAGE', lastMessage)
+ if (lastMessage) {
+ this.dispatch('CHAT_MESSAGE', lastMessage)
+ }
}, 15000)
}