{{ theme.skipToContentLabel || 'Skip to content' }}

Geofences Page Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add a geofence management page with Google Maps, supporting view/edit modes, CRUD operations (polygon/circle/rectangle), grouped/flat list views, and optional heatmap overlay.

Architecture: New feature at src/features/geofences/ following the project's feature-based pattern. Static service class for Supabase CRUD, custom hooks for state, Google Maps with Drawing Manager for shape creation/editing. Reuses heatmap's Google Maps loader and database service for overlay.

Tech Stack: React 19, TypeScript 5, Google Maps JavaScript API (maps, drawing, visualization libraries), Supabase, React Hook Form + Zod, Tailwind CSS


Task 1: Add geofences Module to RBAC System

Files:

  • Modify: src/features/user-management/types/index.ts

Step 1: Add geofences to AppModule type

In src/features/user-management/types/index.ts, add "geofences" to the AppModule type union (after "stock_management"):

typescript
export type AppModule =
  | "dashboard"
  | "dust_levels"
  | "flow_meter"
  | "heatmap"
  | "reports"
  | "weekly_reports"
  | "assets"
  | "climate"
  | "settings"
  | "email_schedules"
  | "dust_ranger"
  | "user_management"
  | "stock_management"
  | "geofences";

Also add to APP_MODULES array:

typescript
export const APP_MODULES: AppModule[] = [
  // ... existing modules
  "stock_management",
  "geofences",
];

And to MODULE_DISPLAY_NAMES:

typescript
export const MODULE_DISPLAY_NAMES: Record<AppModule, string> = {
  // ... existing entries
  stock_management: "Stock Management",
  geofences: "Geofences",
};

Step 2: Verify build

Run: cd /Users/jackqin/Projects/Dashboard/.worktrees/geofences && pnpm build 2>&1 | tail -5 Expected: Build succeeds

Step 3: Commit

bash
git add src/features/user-management/types/index.ts
git commit -m "feat(geofences): add geofences module to RBAC system"

Task 2: Add RLS Policies for Authenticated User Writes

Files:

  • Create: supabase/migrations/20260218100000_add_geofence_write_policies.sql

Step 1: Create migration

Currently only SELECT is allowed for authenticated users. We need INSERT, UPDATE, DELETE for geofence management.

sql
-- Allow authenticated users to insert geofences
CREATE POLICY "Authenticated users can insert data_geofences"
    ON data_geofences FOR INSERT
    WITH CHECK (auth.uid() IS NOT NULL);

CREATE POLICY "Authenticated users can update data_geofences"
    ON data_geofences FOR UPDATE
    USING (auth.uid() IS NOT NULL);

CREATE POLICY "Authenticated users can delete data_geofences"
    ON data_geofences FOR DELETE
    USING (auth.uid() IS NOT NULL);

-- Allow authenticated users to insert geofence groups
CREATE POLICY "Authenticated users can insert data_geofence_groups"
    ON data_geofence_groups FOR INSERT
    WITH CHECK (auth.uid() IS NOT NULL);

CREATE POLICY "Authenticated users can update data_geofence_groups"
    ON data_geofence_groups FOR UPDATE
    USING (auth.uid() IS NOT NULL);

Step 2: Commit

bash
git add supabase/migrations/20260218100000_add_geofence_write_policies.sql
git commit -m "feat(geofences): add RLS write policies for geofence tables"

Task 3: Define TypeScript Types

Files:

  • Create: src/features/geofences/types.ts

Step 1: Create types file

typescript
/** Point coordinate from the API JSON format: { x: longitude, y: latitude } */
export interface GeofencePoint {
  x: number; // longitude
  y: number; // latitude
}

/** Google Maps LatLng literal */
export interface LatLngPoint {
  lat: number;
  lng: number;
}

/** Geofence group from data_geofence_groups table */
export interface GeofenceGroup {
  id: number;
  mine_site_id: string | null;
  site_name: string;
  sss_client_id: string | null;
  geofence_group_code: string;
  name: string | null;
  description: string | null;
  colour_code: string | null;
  geofence_count: number;
  deleted: boolean;
  scraped_at: string | null;
  created_at: string | null;
  updated_at: string | null;
}

/** Geofence record from data_geofences table */
export interface Geofence {
  id: number;
  task_id: string | null;
  mine_site_id: string | null;
  site_name: string;
  sss_client_id: string | null;
  geofence_group_code: string;
  geofence_group_name: string | null;
  geofence_id: string;
  geofence_name: string | null;
  geofence_type_code: string | null;
  note: string | null;
  points: string | null; // JSON string of GeofencePoint[]
  point_count: number;
  min_latitude: number | null;
  max_latitude: number | null;
  min_longitude: number | null;
  max_longitude: number | null;
  map_fill_colour: string | null;
  display_on_map: boolean;
  colour_code: string | null;
  is_enabled: boolean | null;
  deleted: boolean;
  scraped_at: string | null;
  created_at: string | null;
  updated_at: string | null;
}

/** Shape type for drawing */
export type GeofenceShapeType = "polygon" | "circle" | "rectangle";

/** Page mode */
export type GeofencePageMode = "view" | "edit";

/** List view mode */
export type GeofenceListViewMode = "grouped" | "flat";

/** Input for creating a new geofence */
export interface CreateGeofenceInput {
  mine_site_id: string | null;
  site_name: string;
  geofence_group_code?: string;
  geofence_group_name?: string;
  geofence_name: string;
  points: string; // JSON string
  point_count: number;
  min_latitude: number;
  max_latitude: number;
  min_longitude: number;
  max_longitude: number;
  map_fill_colour?: string;
  colour_code?: string;
  display_on_map?: boolean;
  is_enabled?: boolean;
  note?: string;
}

/** Input for updating a geofence */
export interface UpdateGeofenceInput {
  geofence_name?: string;
  points?: string;
  point_count?: number;
  min_latitude?: number;
  max_latitude?: number;
  min_longitude?: number;
  max_longitude?: number;
  map_fill_colour?: string;
  colour_code?: string;
  display_on_map?: boolean;
  is_enabled?: boolean;
  note?: string;
  geofence_group_code?: string;
  geofence_group_name?: string;
}

Step 2: Commit

bash
git add src/features/geofences/types.ts
git commit -m "feat(geofences): add TypeScript type definitions"

Task 4: Create Constants and Utilities

Files:

  • Create: src/features/geofences/constants.ts
  • Create: src/features/geofences/utils.ts

Step 1: Create constants

typescript
/** Default fill colours for new geofences */
export const DEFAULT_GEOFENCE_COLORS = [
  "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4",
  "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F",
];

/** Default fill opacity */
export const DEFAULT_FILL_OPACITY = 0.3;

/** Default stroke opacity */
export const DEFAULT_STROKE_OPACITY = 0.8;

/** Default stroke weight */
export const DEFAULT_STROKE_WEIGHT = 2;

/** Minimum vertices for a polygon */
export const MIN_POLYGON_VERTICES = 3;

/** Number of points to approximate a circle */
export const CIRCLE_APPROXIMATION_POINTS = 64;

/** Custom geofence group name for manually created geofences */
export const CUSTOM_GROUP_NAME = "Custom";

/** Default map center (Pilbara region) */
export const DEFAULT_MAP_CENTER = { lat: -22.6557, lng: 118.1893 };

/** Default map zoom */
export const DEFAULT_MAP_ZOOM = 13;

Step 2: Create utils

typescript
import type { GeofencePoint, LatLngPoint } from "./types";
import { CIRCLE_APPROXIMATION_POINTS } from "./constants";

/** Parse JSON points string to GeofencePoint array */
export function parsePoints(pointsJson: string | null): GeofencePoint[] {
  if (!pointsJson) return [];
  try {
    const parsed = JSON.parse(pointsJson);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(
      (p: unknown): p is GeofencePoint =>
        typeof p === "object" && p !== null && "x" in p && "y" in p
    );
  } catch {
    return [];
  }
}

/** Convert GeofencePoint[] (x=lng, y=lat) to Google Maps LatLng[] */
export function pointsToLatLng(points: GeofencePoint[]): LatLngPoint[] {
  return points.map((p) => ({ lat: p.y, lng: p.x }));
}

/** Convert Google Maps LatLng[] to GeofencePoint[] */
export function latLngToPoints(latLngs: LatLngPoint[]): GeofencePoint[] {
  return latLngs.map((ll) => ({ x: ll.lng, y: ll.lat }));
}

/** Calculate bounding box from LatLng points */
export function calculateBoundingBox(points: LatLngPoint[]): {
  min_latitude: number;
  max_latitude: number;
  min_longitude: number;
  max_longitude: number;
} {
  const lats = points.map((p) => p.lat);
  const lngs = points.map((p) => p.lng);
  return {
    min_latitude: Math.min(...lats),
    max_latitude: Math.max(...lats),
    min_longitude: Math.min(...lngs),
    max_longitude: Math.max(...lngs),
  };
}

/** Convert a circle (center + radius) to polygon points */
export function circleToPolygonPoints(
  center: LatLngPoint,
  radiusMeters: number,
  numPoints: number = CIRCLE_APPROXIMATION_POINTS
): LatLngPoint[] {
  const points: LatLngPoint[] = [];
  const earthRadius = 6371000;
  for (let i = 0; i < numPoints; i++) {
    const angle = (2 * Math.PI * i) / numPoints;
    const lat =
      center.lat +
      (radiusMeters / earthRadius) * (180 / Math.PI) * Math.cos(angle);
    const lng =
      center.lng +
      ((radiusMeters / earthRadius) * (180 / Math.PI) * Math.sin(angle)) /
        Math.cos((center.lat * Math.PI) / 180);
    points.push({ lat, lng });
  }
  return points;
}

/** Convert a rectangle (bounds) to polygon points */
export function rectangleToPolygonPoints(
  north: number,
  south: number,
  east: number,
  west: number
): LatLngPoint[] {
  return [
    { lat: north, lng: west },
    { lat: north, lng: east },
    { lat: south, lng: east },
    { lat: south, lng: west },
  ];
}

/** Generate a new UUID for manually created geofences */
export function generateGeofenceId(): string {
  return crypto.randomUUID();
}

/** Pick a random default color */
export function getRandomColor(colors: string[]): string {
  return colors[Math.floor(Math.random() * colors.length)];
}

Step 3: Commit

bash
git add src/features/geofences/constants.ts src/features/geofences/utils.ts
git commit -m "feat(geofences): add constants and utility functions"

Task 5: Create Geofence Service

Files:

  • Create: src/features/geofences/services/geofenceService.ts

Step 1: Create the service

typescript
import { supabase } from "@/lib/supabase";
import type {
  Geofence,
  GeofenceGroup,
  CreateGeofenceInput,
  UpdateGeofenceInput,
} from "../types";

export class GeofenceService {
  /** Fetch all non-deleted geofences for a mine site */
  static async fetchGeofences(mineSiteId: string): Promise<Geofence[]> {
    const { data, error } = await supabase
      .from("data_geofences")
      .select("*")
      .eq("mine_site_id", mineSiteId)
      .eq("deleted", false)
      .order("geofence_group_name", { ascending: true })
      .order("geofence_name", { ascending: true });

    if (error) throw error;
    return (data ?? []) as unknown as Geofence[];
  }

  /** Fetch all non-deleted geofence groups for a mine site */
  static async fetchGeofenceGroups(
    mineSiteId: string
  ): Promise<GeofenceGroup[]> {
    const { data, error } = await supabase
      .from("data_geofence_groups")
      .select("*")
      .eq("mine_site_id", mineSiteId)
      .eq("deleted", false)
      .order("name", { ascending: true });

    if (error) throw error;
    return (data ?? []) as unknown as GeofenceGroup[];
  }

  /** Create a new geofence */
  static async createGeofence(input: CreateGeofenceInput): Promise<Geofence> {
    const geofenceId = crypto.randomUUID();
    const { data, error } = await supabase
      .from("data_geofences")
      .insert({
        ...input,
        geofence_id: geofenceId,
        geofence_group_code: input.geofence_group_code ?? crypto.randomUUID(),
        deleted: false,
      })
      .select()
      .single();

    if (error) throw error;
    return data as unknown as Geofence;
  }

  /** Update an existing geofence */
  static async updateGeofence(
    id: number,
    input: UpdateGeofenceInput
  ): Promise<Geofence> {
    const { data, error } = await supabase
      .from("data_geofences")
      .update(input)
      .eq("id", id)
      .select()
      .single();

    if (error) throw error;
    return data as unknown as Geofence;
  }

  /** Soft delete a geofence */
  static async deleteGeofence(id: number): Promise<void> {
    const { error } = await supabase
      .from("data_geofences")
      .update({ deleted: true })
      .eq("id", id);

    if (error) throw error;
  }
}

Step 2: Commit

bash
git add src/features/geofences/services/geofenceService.ts
git commit -m "feat(geofences): add geofence service with CRUD operations"

Task 6: Create useGeofences Hook

Files:

  • Create: src/features/geofences/hooks/useGeofences.ts

Step 1: Create the hook

typescript
import { useCallback, useEffect, useState } from "react";
import { GeofenceService } from "../services/geofenceService";
import type {
  Geofence,
  GeofenceGroup,
  GeofencePageMode,
  GeofenceListViewMode,
  CreateGeofenceInput,
  UpdateGeofenceInput,
} from "../types";

export function useGeofences(mineSiteId: string | null) {
  const [geofences, setGeofences] = useState<Geofence[]>([]);
  const [groups, setGroups] = useState<GeofenceGroup[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [selectedGeofenceId, setSelectedGeofenceId] = useState<number | null>(null);
  const [pageMode, setPageMode] = useState<GeofencePageMode>("view");
  const [listViewMode, setListViewMode] = useState<GeofenceListViewMode>("grouped");

  const selectedGeofence = geofences.find((g) => g.id === selectedGeofenceId) ?? null;

  const fetchData = useCallback(async () => {
    if (!mineSiteId) {
      setGeofences([]);
      setGroups([]);
      return;
    }
    setLoading(true);
    setError(null);
    try {
      const [geofenceData, groupData] = await Promise.all([
        GeofenceService.fetchGeofences(mineSiteId),
        GeofenceService.fetchGeofenceGroups(mineSiteId),
      ]);
      setGeofences(geofenceData);
      setGroups(groupData);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to fetch geofences");
    } finally {
      setLoading(false);
    }
  }, [mineSiteId]);

  useEffect(() => {
    void fetchData();
  }, [fetchData]);

  const createGeofence = useCallback(
    async (input: CreateGeofenceInput) => {
      try {
        const created = await GeofenceService.createGeofence(input);
        setGeofences((prev) => [...prev, created]);
        setSelectedGeofenceId(created.id);
        return created;
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to create geofence");
        throw err;
      }
    },
    []
  );

  const updateGeofence = useCallback(
    async (id: number, input: UpdateGeofenceInput) => {
      try {
        const updated = await GeofenceService.updateGeofence(id, input);
        setGeofences((prev) => prev.map((g) => (g.id === id ? updated : g)));
        return updated;
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to update geofence");
        throw err;
      }
    },
    []
  );

  const deleteGeofence = useCallback(
    async (id: number) => {
      try {
        await GeofenceService.deleteGeofence(id);
        setGeofences((prev) => prev.filter((g) => g.id !== id));
        if (selectedGeofenceId === id) setSelectedGeofenceId(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to delete geofence");
        throw err;
      }
    },
    [selectedGeofenceId]
  );

  return {
    geofences,
    groups,
    loading,
    error,
    selectedGeofence,
    selectedGeofenceId,
    setSelectedGeofenceId,
    pageMode,
    setPageMode,
    listViewMode,
    setListViewMode,
    createGeofence,
    updateGeofence,
    deleteGeofence,
    refresh: fetchData,
  };
}

Step 2: Commit

bash
git add src/features/geofences/hooks/useGeofences.ts
git commit -m "feat(geofences): add useGeofences hook with CRUD and state management"

Task 7: Create useGeofenceHeatmap Hook

Files:

  • Create: src/features/geofences/hooks/useGeofenceHeatmap.ts

Step 1: Create the hook

This hook reuses the heatmap feature's database service to fetch heatmap data for the overlay.

typescript
import { useCallback, useState } from "react";
import { fetchHeatmapData } from "@/features/heatmap/services/databaseService";
import type { HeatmapRecord } from "@/features/heatmap/types";

export function useGeofenceHeatmap() {
  const [heatmapData, setHeatmapData] = useState<HeatmapRecord[]>([]);
  const [heatmapEnabled, setHeatmapEnabled] = useState(false);
  const [heatmapLoading, setHeatmapLoading] = useState(false);
  const [heatmapOpacity, setHeatmapOpacity] = useState(0.6);

  const loadHeatmapData = useCallback(
    async (
      mineSiteId: string,
      startDate: Date,
      endDate: Date,
      assetIds?: string[]
    ) => {
      setHeatmapLoading(true);
      try {
        const data = await fetchHeatmapData({
          mineSiteId,
          startDate,
          endDate,
          assetIds,
        });
        setHeatmapData(data);
      } catch (err) {
        console.error("Failed to load heatmap data:", err);
        setHeatmapData([]);
      } finally {
        setHeatmapLoading(false);
      }
    },
    []
  );

  const clearHeatmapData = useCallback(() => {
    setHeatmapData([]);
  }, []);

  return {
    heatmapData,
    heatmapEnabled,
    setHeatmapEnabled,
    heatmapLoading,
    heatmapOpacity,
    setHeatmapOpacity,
    loadHeatmapData,
    clearHeatmapData,
  };
}

Step 2: Verify the heatmap databaseService exports

Check that fetchHeatmapData is exported from src/features/heatmap/services/databaseService.ts and accepts the filter shape used above. Adjust the import/call if the function signature differs.

Step 3: Commit

bash
git add src/features/geofences/hooks/useGeofenceHeatmap.ts
git commit -m "feat(geofences): add useGeofenceHeatmap hook for heatmap overlay"

Task 8: Create GeofenceMap Component

Files:

  • Create: src/features/geofences/components/GeofenceMap.tsx

Step 1: Create the map component

This is the core component. It renders Google Maps, displays geofences as polygons, handles Drawing Manager for creating new shapes, and supports editable mode.

Key responsibilities:

  • Render all geofences as google.maps.Polygon on the map
  • In edit mode: enable Drawing Manager with polygon/circle/rectangle tools
  • In edit mode: make selected geofence editable (draggable vertices, right-click to delete vertex)
  • Handle click on geofence → select it
  • Render heatmap overlay when enabled
  • Fly to geofence when selected from side panel
  • Use useGoogleMapsApi from heatmap feature with libraries: ["visualization", "drawing"]

The component should:

  1. Accept props: geofences, selectedGeofenceId, onSelectGeofence, onCreateGeofence, onUpdateGeofencePoints, pageMode, heatmapData, heatmapEnabled, heatmapOpacity, mapCenter, mapZoom
  2. Use useRef for the map instance, polygon refs, drawing manager ref, heatmap layer ref
  3. On geofence data change: clear old polygons, render new ones
  4. On selected geofence change: set editable (if edit mode), pan to it
  5. On drawing complete: convert shape to polygon points, call onCreateGeofence
  6. On heatmap toggle: create/destroy google.maps.visualization.HeatmapLayer

Implementation should be ~300-400 lines. Build incrementally:

  • First: map initialization + geofence polygon rendering
  • Then: click selection + fly-to
  • Then: Drawing Manager integration
  • Then: editable polygons (vertex drag, right-click delete)
  • Then: heatmap overlay

Step 2: Commit

bash
git add src/features/geofences/components/GeofenceMap.tsx
git commit -m "feat(geofences): add GeofenceMap component with Google Maps integration"

Task 9: Create Side Panel Components

Files:

  • Create: src/features/geofences/components/GeofenceSidePanel.tsx
  • Create: src/features/geofences/components/GeofenceList.tsx
  • Create: src/features/geofences/components/GeofenceGroupList.tsx
  • Create: src/features/geofences/components/GeofenceListItem.tsx
  • Create: src/features/geofences/components/GeofenceDetailForm.tsx
  • Create: src/features/geofences/components/HeatmapOverlayToggle.tsx

Step 1: Create GeofenceListItem

Single geofence row showing: color swatch, name, point count, enabled badge. Click to select. In edit mode: shows edit/delete icons.

Step 2: Create GeofenceList (flat view)

Flat list of all geofences using GeofenceListItem. Optional search/filter input at top.

Step 3: Create GeofenceGroupList (grouped view)

Collapsible accordion groups. Each group header shows group name, color, geofence count. Expand to show GeofenceListItem children. Manual geofences grouped under "Custom".

Step 4: Create HeatmapOverlayToggle

Toggle switch for heatmap on/off. When on, show opacity slider (0.1-1.0). Show loading spinner when heatmap data is loading.

Step 5: Create GeofenceDetailForm

Form for viewing/editing selected geofence properties:

  • geofence_name (text input)
  • note (textarea)
  • map_fill_colour / colour_code (color picker)
  • display_on_map (toggle)
  • is_enabled (toggle)
  • geofence_group_code (dropdown of available groups)
  • Point count (read-only)
  • Bounding box (read-only)

In view mode: all fields read-only. In edit mode: fields editable with Save/Cancel buttons. Use React Hook Form + Zod for validation. Minimum: geofence_name required.

Step 6: Create GeofenceSidePanel

Container component that assembles:

  1. View/Edit mode toggle button (wrapped in PermissionGate with requireEdit)
  2. List view mode toggle (grouped/flat)
  3. Heatmap overlay toggle
  4. Geofence list (grouped or flat based on toggle)
  5. Selected geofence detail form (shown when a geofence is selected)

Collapsible panel with a toggle button on the edge.

Step 7: Commit

bash
git add src/features/geofences/components/
git commit -m "feat(geofences): add side panel components with list views and detail form"

Task 10: Create GeofencesPage Component

Files:

  • Create: src/features/geofences/components/GeofencesPage.tsx

Step 1: Create the page component

This is the main page that orchestrates everything:

typescript
// Pseudocode structure
export default function GeofencesPage() {
  // Get global filters (mine site, date range)
  const { selectedSiteId, startDate, endDate } = useGlobalFilters();

  // Geofence data + state
  const {
    geofences, groups, loading, error,
    selectedGeofence, selectedGeofenceId, setSelectedGeofenceId,
    pageMode, setPageMode, listViewMode, setListViewMode,
    createGeofence, updateGeofence, deleteGeofence, refresh,
  } = useGeofences(selectedSiteId);

  // Heatmap overlay
  const {
    heatmapData, heatmapEnabled, setHeatmapEnabled,
    heatmapLoading, heatmapOpacity, setHeatmapOpacity,
    loadHeatmapData, clearHeatmapData,
  } = useGeofenceHeatmap();

  // Load heatmap data when enabled + filters change
  useEffect(() => {
    if (heatmapEnabled && selectedSiteId && startDate && endDate) {
      loadHeatmapData(selectedSiteId, startDate, endDate);
    } else {
      clearHeatmapData();
    }
  }, [heatmapEnabled, selectedSiteId, startDate, endDate]);

  // Map center from mine site or geofence bounds
  // ...

  return (
    <div className="relative flex h-full w-full">
      <GeofenceMap ... />
      <GeofenceSidePanel ... />
    </div>
  );
}

Step 2: Commit

bash
git add src/features/geofences/components/GeofencesPage.tsx
git commit -m "feat(geofences): add main GeofencesPage component"

Task 11: Register Route and Sidebar Menu

Files:

  • Modify: src/routes/Routes.tsx
  • Modify: src/components/layouts/SideNav/menu.ts

Step 1: Add lazy import in Routes.tsx

After line 160 (const HeatMap = lazy(...)), add:

typescript
const Geofences = lazy(() => import("@/app/(admin)/(pages)/geofences"));

Step 2: Add route config in layoutsRoutes

After the heatmap route (line 543), add:

typescript
{
  path: "/geofences",
  name: "Geofences",
  element: <Geofences />,
  requiredModule: "geofences",
  showDatePicker: true,
  showSiteSelector: true,
},

Step 3: Create page entry point

Create src/app/(admin)/(pages)/geofences/index.tsx:

typescript
import GeofencesPage from "@/features/geofences/components/GeofencesPage";

export default function Geofences() {
  return <GeofencesPage />;
}

Step 4: Add sidebar menu item

In src/components/layouts/SideNav/menu.ts, add after the HeatMap entry (line 154), import TbFence from react-icons/tb (or use existing FaMapLocationDot):

typescript
{
  key: "Geofences",
  label: "Geofences",
  icon: FaDrawPolygon,
  href: "/geofences",
  requiredModule: "geofences",
},

Add import at top: import { FaDrawPolygon } from "react-icons/fa6";

Step 5: Verify build

Run: cd /Users/jackqin/Projects/Dashboard/.worktrees/geofences && pnpm build 2>&1 | tail -10 Expected: Build succeeds

Step 6: Commit

bash
git add src/routes/Routes.tsx src/components/layouts/SideNav/menu.ts src/app/\(admin\)/\(pages\)/geofences/index.tsx
git commit -m "feat(geofences): register route and sidebar menu item"

Task 12: Integration Testing and Polish

Step 1: Run full test suite

bash
cd /Users/jackqin/Projects/Dashboard/.worktrees/geofences && pnpm test:unit 2>&1 | tail -10

Expected: All existing tests pass.

Step 2: Run lint

bash
pnpm lint 2>&1 | tail -20

Fix any lint issues.

Step 3: Run build

bash
pnpm build 2>&1 | tail -10

Expected: Build succeeds with no TypeScript errors.

Step 4: Final commit

bash
git add -A
git commit -m "feat(geofences): polish and fix lint issues"

Task Dependency Order

Task 1 (RBAC) → Task 2 (RLS) → Task 3 (Types) → Task 4 (Constants/Utils)
  → Task 5 (Service) → Task 6 (useGeofences Hook) → Task 7 (useGeofenceHeatmap Hook)
  → Task 8 (GeofenceMap) → Task 9 (Side Panel Components) → Task 10 (GeofencesPage)
  → Task 11 (Route + Menu) → Task 12 (Testing + Polish)

All tasks are sequential. Each builds on the previous.