4884961c8e
Add the database foundation for mobile groomer route optimization: - clients: latitude/longitude (double precision) + geocodedAt - groomer_routes: per-(staff, date) route with route_status enum, totals, optimizedAt; UNIQUE(staff_id, route_date) - route_stops: ordered stops FK->groomer_routes (cascade) + appointments, lat/lng, per-leg travel mins/distance, bufferMins; UNIQUE(route_id, appointment_id) and UNIQUE(route_id, stop_order) - business_settings: defaultTravelBufferMins (default 15), routeOptimizationProvider (default nominatim), googleMapsApiKey (encrypted at rest at the app layer) - Idempotent hand-authored migration 0041 + journal entry (when=max+1) Lands in packages/db (the deployed schema/migration source per the Dockerfile migrate stage); apps/api is the legacy CI-only copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.3 KiB
SQL
67 lines
3.3 KiB
SQL
-- Migration: 0041_route_optimization.sql
|
|
-- Route optimization schema: geocoding columns on clients, groomerRoutes +
|
|
-- routeStops tables, and route settings on business_settings.
|
|
-- Written idempotently so it is safe to re-run.
|
|
|
|
-- ─── Enums ────────────────────────────────────────────────────────────────────
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE "route_status" AS ENUM ('draft', 'optimized', 'in_progress', 'completed');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- ─── Clients: geocoding columns ───────────────────────────────────────────────
|
|
|
|
ALTER TABLE "clients" ADD COLUMN IF NOT EXISTS "latitude" double precision;
|
|
ALTER TABLE "clients" ADD COLUMN IF NOT EXISTS "longitude" double precision;
|
|
ALTER TABLE "clients" ADD COLUMN IF NOT EXISTS "geocoded_at" timestamp;
|
|
|
|
-- ─── Business settings: route optimization config ─────────────────────────────
|
|
|
|
ALTER TABLE "business_settings"
|
|
ADD COLUMN IF NOT EXISTS "default_travel_buffer_mins" integer NOT NULL DEFAULT 15;
|
|
ALTER TABLE "business_settings"
|
|
ADD COLUMN IF NOT EXISTS "route_optimization_provider" text DEFAULT 'nominatim';
|
|
-- Encrypted at rest at the application layer (AES-256-GCM).
|
|
ALTER TABLE "business_settings"
|
|
ADD COLUMN IF NOT EXISTS "google_maps_api_key" text;
|
|
|
|
-- ─── Groomer routes table ─────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS "groomer_routes" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
"staff_id" uuid NOT NULL REFERENCES "staff"("id") ON DELETE CASCADE,
|
|
"route_date" date NOT NULL,
|
|
"status" "route_status" NOT NULL DEFAULT 'draft',
|
|
"total_travel_mins" integer,
|
|
"total_distance_km" numeric(8, 2),
|
|
"optimized_at" timestamp,
|
|
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
"updated_at" timestamp NOT NULL DEFAULT now(),
|
|
CONSTRAINT "uq_groomer_routes_staff_date" UNIQUE ("staff_id", "route_date")
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS "idx_groomer_routes_staff_id"
|
|
ON "groomer_routes"("staff_id");
|
|
|
|
-- ─── Route stops table ────────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS "route_stops" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
"route_id" uuid NOT NULL REFERENCES "groomer_routes"("id") ON DELETE CASCADE,
|
|
"appointment_id" uuid NOT NULL REFERENCES "appointments"("id") ON DELETE CASCADE,
|
|
"stop_order" integer NOT NULL,
|
|
"latitude" double precision NOT NULL,
|
|
"longitude" double precision NOT NULL,
|
|
"travel_mins_from_prev" integer,
|
|
"travel_distance_km_from_prev" numeric(8, 2),
|
|
"buffer_mins" integer NOT NULL DEFAULT 15,
|
|
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
"updated_at" timestamp NOT NULL DEFAULT now(),
|
|
CONSTRAINT "uq_route_stops_route_appointment" UNIQUE ("route_id", "appointment_id"),
|
|
CONSTRAINT "uq_route_stops_route_order" UNIQUE ("route_id", "stop_order")
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS "idx_route_stops_route_id"
|
|
ON "route_stops"("route_id");
|