5.7 KiB
5.7 KiB
My Routes Page
Overview
File Path: admin/src/pages/volunteer/MyRoutesPage.tsx (275 lines)
Route: /volunteer/routes
Role Requirements: Authenticated users
Purpose: Visual display of volunteer's canvassing routes with map visualization and session history.
Key Features:
- Statistics cards (Total Sessions, Total Distance, Total Time)
- Interactive map with route polyline
- Color-coded event markers (Session Start/End, Visit, Location Added)
- Legend for event types
- Session history table (date, duration, distance, point count)
- View/Hide route toggle button
- FitBounds component to center map on route
- Dark CARTO basemap
Features
1. Statistics Summary
<Row gutter={16}>
<Col xs={24} sm={8}>
<Statistic
title="Total Sessions"
value={stats.totalSessions}
prefix={<ClockCircleOutlined />}
/>
</Col>
<Col xs={24} sm={8}>
<Statistic
title="Total Distance"
value={`${(stats.totalDistance / 1000).toFixed(1)} km`}
prefix={<EnvironmentOutlined />}
/>
</Col>
<Col xs={24} sm=8}>
<Statistic
title="Total Time"
value={formatDuration(stats.totalDuration)}
prefix={<FieldTimeOutlined />}
/>
</Col>
</Row>
2. Route Map
<MapContainer
center={[45.5017, -73.5673]}
zoom={13}
style={{ height: 400 }}
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution='© <a href="https://carto.com/">CARTO</a>'
/>
{/* Route polyline */}
{routeVisible && selectedRoute && (
<Polyline
positions={selectedRoute.points.map(p => [p.latitude, p.longitude])}
pathOptions={{ color: '#1890ff', weight: 3 }}
/>
)}
{/* Event markers */}
{selectedRoute?.points.map(point => (
<CircleMarker
key={point.id}
center={[point.latitude, point.longitude]}
radius={6}
pathOptions={{
color: getEventColor(point.eventType),
fillColor: getEventColor(point.eventType),
fillOpacity: 1
}}
>
<Popup>
<Text strong>{point.eventType}</Text>
<br />
<Text type="secondary">
{dayjs(point.timestamp).format('h:mm:ss A')}
</Text>
</Popup>
</CircleMarker>
))}
{/* Fit bounds to route */}
{selectedRoute && <FitBounds points={selectedRoute.points} />}
</MapContainer>
3. Event Type Legend
<div style={{ padding: 12, background: 'rgba(0,0,0,0.7)', borderRadius: 4 }}>
<Space direction="vertical" size={4}>
<Space>
<div style={{ width: 12, height: 12, background: '#52c41a', borderRadius: '50%' }} />
<Text style={{ color: 'white', fontSize: 12 }}>Session Start</Text>
</Space>
<Space>
<div style={{ width: 12, height: 12, background: '#f5222d', borderRadius: '50%' }} />
<Text style={{ color: 'white', fontSize: 12 }}>Session End</Text>
</Space>
<Space>
<div style={{ width: 12, height: 12, background: '#1890ff', borderRadius: '50%' }} />
<Text style={{ color: 'white', fontSize: 12 }}>Visit</Text>
</Space>
<Space>
<div style={{ width: 12, height: 12, background: '#722ed1', borderRadius: '50%' }} />
<Text style={{ color: 'white', fontSize: 12 }}>Location Added</Text>
</Space>
</Space>
</div>
4. Session History Table
<Table
dataSource={sessions}
columns={[
{
title: 'Date',
dataIndex: 'startTime',
render: (date) => dayjs(date).format('MMM D, YYYY')
},
{
title: 'Duration',
key: 'duration',
render: (_, record) => {
const start = dayjs(record.startTime);
const end = dayjs(record.endTime);
return formatDuration(end.diff(start, 'seconds'));
}
},
{
title: 'Distance',
dataIndex: 'distance',
render: (distance) => `${(distance / 1000).toFixed(1)} km`
},
{
title: 'Points',
dataIndex: 'pointCount',
render: (count) => `${count} points`
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Button
type="link"
onClick={() => {
setSelectedRoute(record);
setRouteVisible(true);
}}
>
View Route
</Button>
)
}
]}
/>
API Integration
Endpoints
1. Get Route Stats
GET /api/map/canvass/my-routes/stats
Authorization: Bearer {token}
Response:
{
"totalSessions": 12,
"totalDistance": 34567,
"totalDuration": 18900
}
2. Get Session Routes
GET /api/map/canvass/my-routes
Authorization: Bearer {token}
Response:
[
{
"sessionId": "cm1session123",
"startTime": "2025-02-12T10:00:00.000Z",
"endTime": "2025-02-12T12:30:00.000Z",
"distance": 2834,
"pointCount": 45,
"points": [
{
"id": "cm1point1",
"latitude": 45.5017,
"longitude": -73.5673,
"eventType": "session_start",
"timestamp": "2025-02-12T10:00:00.000Z"
}
]
}
]
Utility Functions
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
};
const getEventColor = (eventType: string): string => {
switch (eventType) {
case 'session_start': return '#52c41a';
case 'session_end': return '#f5222d';
case 'visit': return '#1890ff';
case 'location_added': return '#722ed1';
default: return '#8c8c8c';
}
};