260 lines
5.7 KiB
Markdown
260 lines
5.7 KiB
Markdown
# 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
|
|
|
|
```tsx
|
|
<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
|
|
|
|
```tsx
|
|
<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
|
|
|
|
```tsx
|
|
<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
|
|
|
|
```tsx
|
|
<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
|
|
```http
|
|
GET /api/map/canvass/my-routes/stats
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"totalSessions": 12,
|
|
"totalDistance": 34567,
|
|
"totalDuration": 18900
|
|
}
|
|
```
|
|
|
|
#### 2. Get Session Routes
|
|
```http
|
|
GET /api/map/canvass/my-routes
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
[
|
|
{
|
|
"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
|
|
|
|
```typescript
|
|
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';
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [My Activity Page](./my-activity-page.md)
|
|
- [Volunteer Map Page](./volunteer-map-page.md)
|
|
- [GPS Tracking System](../../../architecture/gps-tracking.md)
|