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"):
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:
export const APP_MODULES: AppModule[] = [
// ... existing modules
"stock_management",
"geofences",
];And to MODULE_DISPLAY_NAMES:
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
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.
-- 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
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
/** 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
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
/** 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
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
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
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
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
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
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.
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
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.Polygonon 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
useGoogleMapsApifrom heatmap feature with libraries:["visualization", "drawing"]
The component should:
- Accept props:
geofences,selectedGeofenceId,onSelectGeofence,onCreateGeofence,onUpdateGeofencePoints,pageMode,heatmapData,heatmapEnabled,heatmapOpacity,mapCenter,mapZoom - Use
useReffor the map instance, polygon refs, drawing manager ref, heatmap layer ref - On geofence data change: clear old polygons, render new ones
- On selected geofence change: set editable (if edit mode), pan to it
- On drawing complete: convert shape to polygon points, call
onCreateGeofence - 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
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:
- View/Edit mode toggle button (wrapped in
PermissionGatewithrequireEdit) - List view mode toggle (grouped/flat)
- Heatmap overlay toggle
- Geofence list (grouped or flat based on toggle)
- Selected geofence detail form (shown when a geofence is selected)
Collapsible panel with a toggle button on the edge.
Step 7: Commit
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:
// 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
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:
const Geofences = lazy(() => import("@/app/(admin)/(pages)/geofences"));Step 2: Add route config in layoutsRoutes
After the heatmap route (line 543), add:
{
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:
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):
{
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
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
cd /Users/jackqin/Projects/Dashboard/.worktrees/geofences && pnpm test:unit 2>&1 | tail -10Expected: All existing tests pass.
Step 2: Run lint
pnpm lint 2>&1 | tail -20Fix any lint issues.
Step 3: Run build
pnpm build 2>&1 | tail -10Expected: Build succeeds with no TypeScript errors.
Step 4: Final commit
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.