This commit is contained in:
michael committed 2026-09-08 16:48:28 +02:00
1 parent 3d9674bd5a
commit d1da86beab
20 files changed
+6109 -923

No files matched your search

+90 -1
View File
@@ -89,6 +89,16 @@ export async function migrate() {
);
`);
await query(`
CREATE TABLE IF NOT EXISTS user_preferences (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, key)
);
`);
await query(`
CREATE TABLE IF NOT EXISTS organizations (
id BIGSERIAL PRIMARY KEY,
@@ -448,14 +458,54 @@ export async function migrate() {
starts_on DATE,
ends_on DATE,
hour_budget_minutes INTEGER,
manual_consumption_minutes INTEGER NOT NULL DEFAULT 0,
closed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, ticket_id),
CONSTRAINT projects_budget_non_negative CHECK (hour_budget_minutes IS NULL OR hour_budget_minutes >= 0),
CONSTRAINT projects_manual_consumption_non_negative CHECK (manual_consumption_minutes >= 0),
CONSTRAINT projects_valid_range CHECK (starts_on IS NULL OR ends_on IS NULL OR ends_on >= starts_on)
);
`);
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;");
await query("ALTER TABLE projects ADD COLUMN IF NOT EXISTS manual_consumption_minutes INTEGER NOT NULL DEFAULT 0;");
await query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'projects_manual_consumption_non_negative'
) THEN
ALTER TABLE projects
ADD CONSTRAINT projects_manual_consumption_non_negative CHECK (manual_consumption_minutes >= 0);
END IF;
END $$;
`);
await query(`
UPDATE tickets t
SET work_type = 'consulting'
WHERE EXISTS (
SELECT 1
FROM sessions s
WHERE s.ticket_id = t.id
AND s.work_type = 'consulting'
);
`);
await query(`
INSERT INTO projects (user_id, ticket_id, teamspace_project, description, goals)
SELECT DISTINCT s.user_id, s.ticket_id, COALESCE(t.title, ''), '', ''
FROM sessions s
JOIN tickets t ON t.id = s.ticket_id
WHERE s.work_type = 'consulting'
AND s.user_id IS NOT NULL
ON CONFLICT (user_id, ticket_id) DO NOTHING;
`);
await query(`
CREATE TABLE IF NOT EXISTS ticket_todos (
id BIGSERIAL PRIMARY KEY,
@@ -465,14 +515,50 @@ export async function migrate() {
description TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'open',
sort_order INTEGER NOT NULL DEFAULT 0,
starts_on DATE,
ends_on DATE,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT ticket_todos_priority_valid CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
CONSTRAINT ticket_todos_status_valid CHECK (status IN ('open', 'done'))
CONSTRAINT ticket_todos_status_valid CHECK (status IN ('open', 'done')),
CONSTRAINT ticket_todos_valid_period CHECK (starts_on IS NULL OR ends_on IS NULL OR ends_on >= starts_on)
);
`);
await query("ALTER TABLE ticket_todos ADD COLUMN IF NOT EXISTS starts_on DATE;");
await query("ALTER TABLE ticket_todos ADD COLUMN IF NOT EXISTS ends_on DATE;");
await query("ALTER TABLE ticket_todos ADD COLUMN IF NOT EXISTS sort_order INTEGER;");
await query(`
WITH ordered_todos AS (
SELECT
id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at ASC, id ASC) AS row_number
FROM ticket_todos
WHERE sort_order IS NULL
)
UPDATE ticket_todos tt
SET sort_order = ordered_todos.row_number * 1024
FROM ordered_todos
WHERE tt.id = ordered_todos.id;
`);
await query("ALTER TABLE ticket_todos ALTER COLUMN sort_order SET DEFAULT 0;");
await query("ALTER TABLE ticket_todos ALTER COLUMN sort_order SET NOT NULL;");
await query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ticket_todos_valid_period'
) THEN
ALTER TABLE ticket_todos
ADD CONSTRAINT ticket_todos_valid_period CHECK (starts_on IS NULL OR ends_on IS NULL OR ends_on >= starts_on);
END IF;
END $$;
`);
await ensureClosureOwnership("ticket_month_closures", "month", ["ticket_id", "month"], adminId);
await ensureClosureOwnership("month_closures", "month", ["month"], adminId);
await ensureClosureOwnership("ticket_day_closures", "day", ["ticket_id", "day"], adminId);
@@ -490,8 +576,11 @@ export async function migrate() {
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billing_ack_user_day ON ticket_day_billing_acknowledgements(user_id, day);");
await query("CREATE INDEX IF NOT EXISTS idx_projects_user_ticket ON projects(user_id, ticket_id);");
await query("CREATE INDEX IF NOT EXISTS idx_projects_user_updated ON projects(user_id, updated_at DESC);");
await query("CREATE INDEX IF NOT EXISTS idx_projects_user_closed_updated ON projects(user_id, closed_at, updated_at DESC);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_todos_user_status_priority ON ticket_todos(user_id, status, priority);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_todos_ticket_user ON ticket_todos(ticket_id, user_id);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_todos_user_period ON ticket_todos(user_id, starts_on, ends_on);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_todos_user_sort_order ON ticket_todos(user_id, sort_order, id);");
await query("CREATE INDEX IF NOT EXISTS idx_recurring_billings_user_active ON recurring_billings(user_id, active);");
await query("UPDATE recurring_billings SET active = true WHERE active = false;");
await query(`