130 lines
6.4 KiB
MySQL
130 lines
6.4 KiB
MySQL
|
|
-- 0001_init — начальная схема.
|
|||
|
|
--
|
|||
|
|
-- Файл рассчитан на два случая:
|
|||
|
|
-- * свежая БД — всё создаётся с нуля;
|
|||
|
|
-- * БД, где таблицы были созданы вручную до появления миграций —
|
|||
|
|
-- CREATE ... IF NOT EXISTS ничего не ломает, а блоки ALTER/DROP/ADD
|
|||
|
|
-- приводят ограничения к эталону (атомарно, в одной транзакции).
|
|||
|
|
--
|
|||
|
|
-- После применения файл редактировать нельзя: раннер хранит checksum
|
|||
|
|
-- и упадёт на старте. Любое изменение — это новая миграция 0002_*.sql.
|
|||
|
|
|
|||
|
|
-- ============ profiles ============
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS profiles (
|
|||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|||
|
|
email text NOT NULL,
|
|||
|
|
password_hash text NOT NULL,
|
|||
|
|
name text NOT NULL,
|
|||
|
|
phone text,
|
|||
|
|
role text NOT NULL DEFAULT 'client',
|
|||
|
|
is_active boolean NOT NULL DEFAULT true,
|
|||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|||
|
|
|
|||
|
|
CONSTRAINT profiles_role_check CHECK (role IN ('client', 'worker', 'admin'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
CREATE UNIQUE INDEX IF NOT EXISTS profiles_email_key ON profiles (email);
|
|||
|
|
CREATE INDEX IF NOT EXISTS profiles_role_idx ON profiles (role) WHERE is_active;
|
|||
|
|
|
|||
|
|
ALTER TABLE profiles DROP CONSTRAINT IF EXISTS profiles_role_check;
|
|||
|
|
ALTER TABLE profiles ADD CONSTRAINT profiles_role_check
|
|||
|
|
CHECK (role IN ('client', 'worker', 'admin'));
|
|||
|
|
|
|||
|
|
-- ============ tickets ============
|
|||
|
|
|
|||
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|||
|
|
id bigserial PRIMARY KEY,
|
|||
|
|
client_id uuid NOT NULL REFERENCES profiles (id),
|
|||
|
|
worker_id uuid REFERENCES profiles (id),
|
|||
|
|
assigned_by uuid REFERENCES profiles (id),
|
|||
|
|
address text NOT NULL,
|
|||
|
|
floor text,
|
|||
|
|
category text NOT NULL,
|
|||
|
|
description text NOT NULL,
|
|||
|
|
priority text NOT NULL DEFAULT 'normal',
|
|||
|
|
status text NOT NULL DEFAULT 'new',
|
|||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|||
|
|
assigned_at timestamptz,
|
|||
|
|
started_at timestamptz,
|
|||
|
|
done_at timestamptz,
|
|||
|
|
confirmed_at timestamptz,
|
|||
|
|
closed_at timestamptz,
|
|||
|
|
|
|||
|
|
CONSTRAINT tickets_category_check
|
|||
|
|
CHECK (category IN ('electric', 'plumbing', 'climate', 'elevator', 'other')),
|
|||
|
|
CONSTRAINT tickets_priority_check
|
|||
|
|
CHECK (priority IN ('low', 'normal', 'high')),
|
|||
|
|
-- 'rejected' оставлен из исходной ручной схемы, 'closed' добавлен,
|
|||
|
|
-- чтобы поле closed_at имело свой статус.
|
|||
|
|
CONSTRAINT tickets_status_check
|
|||
|
|
CHECK (status IN ('new', 'assigned', 'in_progress', 'done',
|
|||
|
|
'confirmed', 'rejected', 'closed')),
|
|||
|
|
|
|||
|
|
-- Ядро модели: свободная заявка ⇔ статус 'new'.
|
|||
|
|
-- Назначение мастера всегда меняет worker_id и status одной операцией,
|
|||
|
|
-- поэтому пул свободных заявок не «протекает» в другие статусы.
|
|||
|
|
CONSTRAINT tickets_free_iff_new
|
|||
|
|
CHECK ((worker_id IS NULL) = (status = 'new')),
|
|||
|
|
|
|||
|
|
-- Назначение фиксируется временем — иначе UI не покажет «когда назначили».
|
|||
|
|
CONSTRAINT tickets_assigned_time
|
|||
|
|
CHECK (worker_id IS NULL OR assigned_at IS NOT NULL),
|
|||
|
|
|
|||
|
|
-- Статусы, которых достигли, обязаны иметь свой таймстамп.
|
|||
|
|
CONSTRAINT tickets_started_time
|
|||
|
|
CHECK (status <> 'in_progress' OR started_at IS NOT NULL),
|
|||
|
|
CONSTRAINT tickets_done_time
|
|||
|
|
CHECK (status <> 'done' OR done_at IS NOT NULL),
|
|||
|
|
CONSTRAINT tickets_confirmed_time
|
|||
|
|
CHECK (status <> 'confirmed' OR confirmed_at IS NOT NULL),
|
|||
|
|
CONSTRAINT tickets_closed_time
|
|||
|
|
CHECK (status <> 'closed' OR closed_at IS NOT NULL)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- Приводим ограничения к эталону, если таблица существовала до миграций.
|
|||
|
|
-- DROP + ADD вместо IF NOT EXISTS: у ALTER TABLE ADD CONSTRAINT его нет.
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_category_check;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_category_check
|
|||
|
|
CHECK (category IN ('electric', 'plumbing', 'climate', 'elevator', 'other'));
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_priority_check;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_priority_check
|
|||
|
|
CHECK (priority IN ('low', 'normal', 'high'));
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_status_check;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_status_check
|
|||
|
|
CHECK (status IN ('new', 'assigned', 'in_progress', 'done',
|
|||
|
|
'confirmed', 'rejected', 'closed'));
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_free_iff_new;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_free_iff_new
|
|||
|
|
CHECK ((worker_id IS NULL) = (status = 'new'));
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_assigned_time;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_assigned_time
|
|||
|
|
CHECK (worker_id IS NULL OR assigned_at IS NOT NULL);
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_started_time;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_started_time
|
|||
|
|
CHECK (status <> 'in_progress' OR started_at IS NOT NULL);
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_done_time;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_done_time
|
|||
|
|
CHECK (status <> 'done' OR done_at IS NOT NULL);
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_confirmed_time;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_confirmed_time
|
|||
|
|
CHECK (status <> 'confirmed' OR confirmed_at IS NOT NULL);
|
|||
|
|
|
|||
|
|
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_closed_time;
|
|||
|
|
ALTER TABLE tickets ADD CONSTRAINT tickets_closed_time
|
|||
|
|
CHECK (status <> 'closed' OR closed_at IS NOT NULL);
|
|||
|
|
|
|||
|
|
-- Ленты: клиент — свои, мастер — назначенные ему, админ/дашборд — по статусу.
|
|||
|
|
-- Имена совпадают с уже существующими в ручной схеме, чтобы не плодить дубли.
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_tickets_client ON tickets (client_id);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_tickets_worker ON tickets (worker_id);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets (status);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_tickets_created_at ON tickets (created_at DESC);
|