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='&copy; <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';
  }
};