-- 0004_projects — проекты, объекты и мастера проекта. -- -- Иерархия (см. README): -- админ создаёт клиента -- └── клиент создаёт проект -- ├── объекты (здания, адреса) — входят в проект -- └── мастера — добавлены в проект (мастер может быть в нескольких) -- └── заявка привязана к объекту и достаётся мастеру проекта -- -- Принцип прежний: принадлежность проверяется на уровне БД, а не только -- в коде. Составные FK: -- tickets (object_id, project_id) → project_objects (id, project_id) -- «объект входит в этот проект»; -- tickets (project_id, client_id) → projects (id, client_id) -- «проект принадлежит клиенту заявки». -- Членство мастера в проекте — не FK, а проверка внутри UPDATE назначения: -- FK отдалял бы уход мастера из проекта навсегда (ссылки остаются от -- закрытых заявок), а «назначить чужого» не даёт ту же строку WHERE. -- -- Применённый файл не редактировать (checksum в schema_migrations). -- Существующие заявки сделаны до появления объектов и не имеют object_id. -- В базе они тестовые (решение принято), удаляем — иначе NOT NULL не ввести. DELETE FROM tickets; -- ============ projects ============ CREATE TABLE IF NOT EXISTS projects ( id bigserial PRIMARY KEY, client_id uuid NOT NULL REFERENCES profiles (id), name text NOT NULL, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); -- Один клиент не держит два проекта с одинаковым именем. ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_client_name_key; ALTER TABLE projects ADD CONSTRAINT projects_client_name_key UNIQUE (client_id, name); -- Цель для составного FK из tickets (project_id, client_id). ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_id_client_key; ALTER TABLE projects ADD CONSTRAINT projects_id_client_key UNIQUE (id, client_id); CREATE INDEX IF NOT EXISTS projects_client_idx ON projects (client_id); -- ============ project_objects ============ CREATE TABLE IF NOT EXISTS project_objects ( id bigserial PRIMARY KEY, project_id bigint NOT NULL REFERENCES projects (id) ON DELETE CASCADE, name text NOT NULL, address text NOT NULL, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE project_objects DROP CONSTRAINT IF EXISTS project_objects_project_name_key; ALTER TABLE project_objects ADD CONSTRAINT project_objects_project_name_key UNIQUE (project_id, name); -- Цель для составного FK из tickets (object_id, project_id). ALTER TABLE project_objects DROP CONSTRAINT IF EXISTS project_objects_id_project_key; ALTER TABLE project_objects ADD CONSTRAINT project_objects_id_project_key UNIQUE (id, project_id); CREATE INDEX IF NOT EXISTS project_objects_project_idx ON project_objects (project_id); -- ============ project_workers ============ -- Мастер ↔ проект: многие-ко-многим (мастер работает на разных объектах -- и в разных проектах одного клиента). CREATE TABLE IF NOT EXISTS project_workers ( worker_id uuid NOT NULL REFERENCES profiles (id), project_id bigint NOT NULL REFERENCES projects (id) ON DELETE CASCADE, added_by uuid REFERENCES profiles (id), created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (worker_id, project_id) ); CREATE INDEX IF NOT EXISTS project_workers_project_idx ON project_workers (project_id); -- ============ tickets ============ ALTER TABLE tickets ADD COLUMN IF NOT EXISTS project_id bigint; ALTER TABLE tickets ADD COLUMN IF NOT EXISTS object_id bigint; -- Заявка обязана жить в объекте: адрес копируется из объекта на момент -- создания (история работ показывает адрес, который был тогда). -- Таблица к этому моменту пуста — SET NOT NULL безопасен. ALTER TABLE tickets ALTER COLUMN project_id SET NOT NULL; ALTER TABLE tickets ALTER COLUMN object_id SET NOT NULL; ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_object_project_fk; ALTER TABLE tickets ADD CONSTRAINT tickets_object_project_fk FOREIGN KEY (object_id, project_id) REFERENCES project_objects (id, project_id); ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_project_client_fk; ALTER TABLE tickets ADD CONSTRAINT tickets_project_client_fk FOREIGN KEY (project_id, client_id) REFERENCES projects (id, client_id); -- Членство мастера: проверяется в UPDATE назначения -- (repository.TicketRepo.Assign), см. комментарий в шапке файла. CREATE INDEX IF NOT EXISTS idx_tickets_project ON tickets (project_id); CREATE INDEX IF NOT EXISTS idx_tickets_object ON tickets (object_id);