feat(time): add span-based attendance tracking under 2.0
This commit is contained in:
93
backend/db/migrations/0080_staff_time_spans.sql
Normal file
93
backend/db/migrations/0080_staff_time_spans.sql
Normal file
@@ -0,0 +1,93 @@
|
||||
CREATE TABLE staff_time_v2_accounts (
|
||||
tenant_id bigint NOT NULL REFERENCES tenants(id), user_id uuid NOT NULL REFERENCES auth_users(id),
|
||||
activated_at timestamptz NOT NULL DEFAULT now(), activated_by uuid REFERENCES auth_users(id),
|
||||
PRIMARY KEY(tenant_id,user_id)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE staff_time_spans (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id bigint NOT NULL REFERENCES tenants(id),
|
||||
user_id uuid NOT NULL REFERENCES auth_users(id),
|
||||
type text NOT NULL CHECK(type IN ('work','vacation','sick','overtime_compensation','holiday','other')),
|
||||
started_at timestamptz NOT NULL, ended_at timestamptz,
|
||||
date_from date, date_to date, timezone text NOT NULL DEFAULT 'Europe/Berlin',
|
||||
description text NOT NULL DEFAULT '', source text NOT NULL,
|
||||
status text NOT NULL DEFAULT 'factual' CHECK(status IN ('factual','submitted','approved','rejected')),
|
||||
version integer NOT NULL DEFAULT 1 CHECK(version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CHECK(ended_at IS NULL OR ended_at > started_at),
|
||||
CHECK((date_from IS NULL AND date_to IS NULL) OR (type <> 'work' AND date_from IS NOT NULL AND date_to IS NOT NULL AND date_to > date_from AND ended_at IS NOT NULL)),
|
||||
CHECK(status = 'factual' OR ended_at IS NOT NULL)
|
||||
);
|
||||
CREATE INDEX staff_time_spans_user_time ON staff_time_spans(tenant_id,user_id,started_at);
|
||||
CREATE UNIQUE INDEX staff_time_spans_one_open ON staff_time_spans(tenant_id,user_id) WHERE ended_at IS NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE staff_time_span_breaks (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), span_id uuid NOT NULL REFERENCES staff_time_spans(id),
|
||||
started_at timestamptz NOT NULL, ended_at timestamptz,
|
||||
CHECK(ended_at IS NULL OR ended_at > started_at)
|
||||
);
|
||||
CREATE INDEX staff_time_span_breaks_parent ON staff_time_span_breaks(span_id);
|
||||
CREATE UNIQUE INDEX staff_time_span_breaks_one_open ON staff_time_span_breaks(span_id) WHERE ended_at IS NULL;
|
||||
CREATE TABLE staff_time_span_projects (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), span_id uuid NOT NULL REFERENCES staff_time_spans(id),
|
||||
project_id bigint NOT NULL REFERENCES projects(id), started_at timestamptz NOT NULL, ended_at timestamptz NOT NULL,
|
||||
notes text NOT NULL DEFAULT '', CHECK(ended_at > started_at)
|
||||
);
|
||||
CREATE INDEX staff_time_span_projects_parent ON staff_time_span_projects(span_id);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE staff_time_span_history (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), span_id uuid NOT NULL REFERENCES staff_time_spans(id),
|
||||
actor_id uuid REFERENCES auth_users(id), action text NOT NULL, reason text NOT NULL DEFAULT '',
|
||||
before_data jsonb, after_data jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX staff_time_span_history_parent ON staff_time_span_history(span_id,created_at);
|
||||
CREATE TABLE staff_time_v2_requests (
|
||||
tenant_id bigint NOT NULL, user_id uuid NOT NULL, request_id text NOT NULL,
|
||||
fingerprint text NOT NULL, response jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY(tenant_id,user_id,request_id)
|
||||
);
|
||||
CREATE TABLE staff_time_span_origins (
|
||||
tenant_id bigint NOT NULL, source_kind text NOT NULL, source_id uuid NOT NULL,
|
||||
span_id uuid NOT NULL REFERENCES staff_time_spans(id), PRIMARY KEY(tenant_id,source_kind,source_id)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
-- The same lock as the service makes cutover atomic with concurrent legacy writers.
|
||||
CREATE FUNCTION staff_time_legacy_write_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE t bigint; u uuid;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN t := OLD.tenant_id; u := OLD.user_id;
|
||||
ELSE t := NEW.tenant_id; u := NEW.user_id; END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended('staff-time:' || t || ':' || u,0));
|
||||
IF EXISTS(SELECT 1 FROM staff_time_v2_accounts WHERE tenant_id=t AND user_id=u) THEN
|
||||
RAISE EXCEPTION 'Zeiterfassung 2.0 ist aktiv. Bitte die neue Erfassung verwenden.' USING ERRCODE='23514';
|
||||
END IF;
|
||||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER staff_time_events_cutover BEFORE INSERT OR UPDATE OR DELETE ON staff_time_events FOR EACH ROW EXECUTE FUNCTION staff_time_legacy_write_guard();
|
||||
CREATE TRIGGER staff_time_entries_cutover BEFORE INSERT OR UPDATE OR DELETE ON staff_time_entries FOR EACH ROW EXECUTE FUNCTION staff_time_legacy_write_guard();
|
||||
--> statement-breakpoint
|
||||
CREATE FUNCTION staff_time_span_overlap_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended('staff-time:' || NEW.tenant_id || ':' || NEW.user_id,0));
|
||||
IF EXISTS(SELECT 1 FROM staff_time_spans s WHERE s.tenant_id=NEW.tenant_id AND s.user_id=NEW.user_id AND s.id<>NEW.id
|
||||
AND s.started_at<COALESCE(NEW.ended_at,'infinity'::timestamptz) AND COALESCE(s.ended_at,'infinity'::timestamptz)>NEW.started_at) THEN
|
||||
RAISE EXCEPTION 'Überlappende Zeitspannen' USING ERRCODE='23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER staff_time_span_no_overlap BEFORE INSERT OR UPDATE ON staff_time_spans FOR EACH ROW EXECUTE FUNCTION staff_time_span_overlap_guard();
|
||||
--> statement-breakpoint
|
||||
CREATE FUNCTION staff_time_legacy_project_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE parent staff_time_entries;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN SELECT * INTO parent FROM staff_time_entries WHERE id=OLD.time_entry_id;
|
||||
ELSE SELECT * INTO parent FROM staff_time_entries WHERE id=NEW.time_entry_id; END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended('staff-time:' || parent.tenant_id || ':' || parent.user_id,0));
|
||||
IF EXISTS(SELECT 1 FROM staff_time_v2_accounts WHERE tenant_id=parent.tenant_id AND user_id=parent.user_id) THEN
|
||||
RAISE EXCEPTION 'Projektzeiten unter 2.0 bearbeiten.' USING ERRCODE='23514';
|
||||
END IF;
|
||||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
CREATE TRIGGER staff_time_project_cutover BEFORE INSERT OR UPDATE OR DELETE ON staff_time_entry_connects FOR EACH ROW EXECUTE FUNCTION staff_time_legacy_project_guard();
|
||||
@@ -540,6 +540,13 @@
|
||||
"when": 1791072000000,
|
||||
"tag": "0079_notification_matrix",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 77,
|
||||
"version": "7",
|
||||
"when": 1791158400000,
|
||||
"tag": "0080_staff_time_spans",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user