Added Task number

This commit is contained in:
2026-02-21 21:37:36 +08:00
parent 8bb69a80af
commit 6238c701c0
7 changed files with 272 additions and 14 deletions
@@ -0,0 +1,66 @@
-- Add a humanreadable, sequential task number that is shown in the UI
-- Format: YYYY-MM-##### (resetting each month).
-- 1. Add the column (nullable for now so we can backfill)
ALTER TABLE tasks
ADD COLUMN task_number text;
-- 2. Backfill existing rows using created_at as the timestamp basis.
DO $$
DECLARE
r RECORD;
prefix text;
cnt int;
BEGIN
FOR r IN
SELECT id, created_at
FROM tasks
WHERE task_number IS NULL
ORDER BY created_at
LOOP
prefix := to_char(r.created_at::timestamp, 'YYYY-MM-');
cnt := (SELECT count(*)
FROM tasks t
WHERE to_char(t.created_at::timestamp, 'YYYY-MM-') = prefix
AND t.created_at <= r.created_at);
UPDATE tasks
SET task_number = prefix || lpad(cnt::text, 5, '0')
WHERE id = r.id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 3. Create trigger function to generate numbers on new inserts
CREATE OR REPLACE FUNCTION tasks_set_task_number()
RETURNS trigger AS $$
DECLARE
prefix text;
seq int;
BEGIN
-- if caller already provided one, do not overwrite
IF NEW.task_number IS NOT NULL THEN
RETURN NEW;
END IF;
prefix := to_char(now(), 'YYYY-MM-');
SELECT COALESCE(MAX((substring(task_number FROM 8))::int), 0) + 1
INTO seq
FROM tasks
WHERE task_number LIKE prefix || '%';
NEW.task_number := prefix || lpad(seq::text, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tasks_set_task_number_before_insert
BEFORE INSERT ON tasks
FOR EACH ROW
EXECUTE FUNCTION tasks_set_task_number();
-- 4. Enforce not-null and uniqueness now that every row has a value
ALTER TABLE tasks
ALTER COLUMN task_number SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_task_number
ON tasks (task_number);
@@ -0,0 +1,51 @@
-- Improve task_number generation to eliminate race conditions.
-- Maintain a separate counter table so concurrent inserts can safely bump the
-- sequence using an atomic upsert. This migration retains the existing
-- trigger name but replaces its body, and creates the helper table.
-- 1. Create counter table
CREATE TABLE IF NOT EXISTS task_number_counters (
year_month text PRIMARY KEY,
counter bigint NOT NULL
);
-- 2. Initialize counters from existing tasks
INSERT INTO task_number_counters(year_month, counter)
SELECT s.prefix, s.cnt
FROM (
SELECT to_char(created_at::timestamp, 'YYYY-MM-') AS prefix,
max((substring(task_number FROM 8))::int) AS cnt
FROM tasks
GROUP BY prefix
) s
ON CONFLICT (year_month) DO UPDATE SET counter = EXCLUDED.counter;
-- 3. Replace trigger function with atomic counter logic
CREATE OR REPLACE FUNCTION tasks_set_task_number()
RETURNS trigger AS $$
DECLARE
prefix text;
seq bigint;
BEGIN
IF NEW.task_number IS NOT NULL THEN
RETURN NEW;
END IF;
prefix := to_char(now(), 'YYYY-MM-');
INSERT INTO task_number_counters(year_month, counter)
VALUES (prefix, 1)
ON CONFLICT (year_month)
DO UPDATE SET counter = task_number_counters.counter + 1
RETURNING counter INTO seq;
NEW.task_number := prefix || lpad(seq::text, 5, '0');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 4. Recreate trigger (drop/recreate to ensure correct function is bound)
DROP TRIGGER IF EXISTS tasks_set_task_number_before_insert ON tasks;
CREATE TRIGGER tasks_set_task_number_before_insert
BEFORE INSERT ON tasks
FOR EACH ROW
EXECUTE FUNCTION tasks_set_task_number();
@@ -0,0 +1,55 @@
-- Atomic insert that generates a unique task_number and inserts a task in one transaction
-- Returns: id (uuid), task_number (text)
CREATE OR REPLACE FUNCTION insert_task_with_number(
p_title text,
p_description text,
p_office_id text,
p_ticket_id text,
p_request_type text,
p_request_type_other text,
p_request_category text,
p_creator_id uuid
)
RETURNS TABLE(id uuid, task_number text)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
-- The numbering trigger on `tasks` will set `task_number` before insert.
-- This RPC inserts the task row and returns the resulting id and task_number.
BEGIN
-- atomically increment (or create) the month counter and use it as the task_number
INSERT INTO task_number_counters(year_month, counter)
VALUES (to_char(now(), 'YYYY-MM-'), 1)
ON CONFLICT (year_month) DO UPDATE
SET counter = task_number_counters.counter + 1
RETURNING counter INTO seq;
-- build the formatted task number
PERFORM seq; -- ensure seq is set
new_task_number := to_char(now(), 'YYYY-MM-') || lpad(seq::text, 5, '0');
INSERT INTO tasks(
title, description, office_id, ticket_id,
request_type, request_type_other, request_category,
creator_id, created_at, task_number
) VALUES (
p_title,
p_description,
CASE WHEN p_office_id IS NULL OR p_office_id = '' THEN NULL ELSE p_office_id::uuid END,
CASE WHEN p_ticket_id IS NULL OR p_ticket_id = '' THEN NULL ELSE p_ticket_id::uuid END,
CASE WHEN p_request_type IS NULL OR p_request_type = '' THEN NULL ELSE p_request_type::request_type END,
p_request_type_other,
CASE WHEN p_request_category IS NULL OR p_request_category = '' THEN NULL ELSE p_request_category::request_category END,
p_creator_id,
now(),
new_task_number
) RETURNING tasks.id, tasks.task_number INTO id, task_number;
RETURN NEXT;
END;
$$;
-- Grant execute to authenticated so client RPC can call it
GRANT EXECUTE ON FUNCTION insert_task_with_number(text,text,text,text,text,text,text,uuid) TO authenticated;