Files
ProjectE/drizzle/0007_custom_task_statuses.sql
T
bot-hermes b814a4788d feat: add 5 PM features — custom statuses, Gantt, quick-add, automations, notifications
- Custom workflow statuses: per-project configurable status definitions
  replacing the fixed task_status enum. Each project defines its own
  workflow with drag-to-reorder, color coding, and category mapping.
- Gantt/timeline view: full project roadmap with task bars, dependency
  arrows, milestone diamonds, zoom levels (day/week/month), and drag
  to reschedule.
- Natural-language quick-add: NLP parser extracts dates, priorities,
  projects, labels, and recurrence from free text. Floating bar with
  'n' shortcut and live parsed preview.
- Automation rules: no-code trigger-action system per project. Triggers
  on status change, task creation, due date approaching. Actions set
  status/priority, add labels, create notifications.
- Notification center: in-app bell icon with unread badge, slide-out
  panel, real-time SSE updates, mark read/all read. Replaces raw
  activity feed dropdown.

Schema: adds status_definitions, automation_rules, notifications tables.
Migrations: 0007, 0008, 0009. 41 NLP parser tests pass.
2026-08-19 10:54:29 +00:00

121 lines
5.5 KiB
SQL

-- Custom workflow statuses: replace the fixed task_status enum
-- (todo/in_progress/done/cancelled) with per-project status_definitions rows.
--
-- Idempotent — safe to run on every deploy BEFORE `drizzle-kit push`. The push
-- then removes the legacy `tasks.status` column and the `task_status` enum type,
-- which is why the backfill must run first:
--
-- 1. Create the status_category enum + status_definitions table if missing.
-- 2. Seed the four default statuses (todo/in_progress/done/cancelled) for
-- every project that has none yet.
-- 3. Backfill tasks.status_id by joining on the legacy `status` column when it
-- still exists; otherwise fall back to each project's default status.
--
-- Fresh installs (no projects table yet) skip the data steps; the API seeds
-- default statuses when a project is created.
-- ─── 1. status_category enum (Postgres has no CREATE TYPE IF NOT EXISTS) ──────
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'status_category') THEN
CREATE TYPE "status_category" AS ENUM ('todo', 'in_progress', 'done', 'cancelled');
END IF;
END $$;
-- ─── 2. status_definitions table ───────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "status_definitions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"project_id" uuid NOT NULL,
"key" text NOT NULL,
"label" text NOT NULL,
"category" "status_category" DEFAULT 'todo' NOT NULL,
"color" text,
"sort_order" integer DEFAULT 0,
"is_default" boolean DEFAULT false,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'status_definitions_project_id_projects_id_fk') THEN
ALTER TABLE "status_definitions" ADD CONSTRAINT "status_definitions_project_id_projects_id_fk"
FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "status_definitions_project_key_idx" ON "status_definitions" USING btree ("project_id","key");
CREATE INDEX IF NOT EXISTS "status_definitions_project_id_idx" ON "status_definitions" USING btree ("project_id");
-- ─── 3. tasks.status_id column ──────────────────────────────────────────────────
ALTER TABLE "tasks" ADD COLUMN IF NOT EXISTS "status_id" uuid;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tasks_status_id_status_definitions_id_fk') THEN
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_status_id_status_definitions_id_fk"
FOREIGN KEY ("status_id") REFERENCES "public"."status_definitions"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS "tasks_status_id_idx" ON "tasks" USING btree ("status_id");
-- ─── 4. Seed default statuses per project ───────────────────────────────────────
DO $$
BEGIN
IF to_regclass('public.projects') IS NOT NULL AND to_regclass('public.status_definitions') IS NOT NULL THEN
EXECUTE '
INSERT INTO "status_definitions" ("project_id", "key", "label", "category", "color", "sort_order", "is_default")
SELECT p."id", d."key", d."label", d."category", d."color", d."sort_order", d."is_default"
FROM "projects" p
CROSS JOIN (VALUES
(''todo'', ''Todo'', ''todo'', ''#94a3b8'', 0, true),
(''in_progress'', ''In Progress'', ''in_progress'', ''#3b82f6'', 1, false),
(''done'', ''Done'', ''done'', ''#22c55e'', 2, false),
(''cancelled'', ''Cancelled'', ''cancelled'', ''#ef4444'', 3, false)
) AS d("key", "label", "category", "color", "sort_order", "is_default")
WHERE NOT EXISTS (
SELECT 1 FROM "status_definitions" sd WHERE sd."project_id" = p."id"
)';
END IF;
END $$;
-- ─── 5. Backfill tasks.status_id ────────────────────────────────────────────────
-- Legacy status column still present → map each task to the status with the
-- same key in its project (preserves the old todo/in_progress/done/cancelled).
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tasks' AND column_name = 'status'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tasks' AND column_name = 'status_id'
) THEN
EXECUTE '
UPDATE "tasks" t
SET "status_id" = sd."id"
FROM "status_definitions" sd
WHERE t."project_id" = sd."project_id"
AND t."status"::text = sd."key"
AND t."status_id" IS NULL';
END IF;
END $$;
-- Legacy column already gone (or never existed) → assign each null-status task
-- its project''s default status so nothing is left un-categorized.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tasks' AND column_name = 'status'
) THEN
EXECUTE '
UPDATE "tasks" t
SET "status_id" = sd."id"
FROM "status_definitions" sd
WHERE sd."project_id" = t."project_id"
AND sd."is_default" = true
AND t."status_id" IS NULL';
END IF;
END $$;