-- -*- coding: utf-8 -*- -- ============================================================================ -- НЕОН CRM — доски заметок/задач (общая + приватные «владелец ↔ админ»). -- -- Идемпотентна: повторный запуск не падает и не меняет данные. -- CRM применяет этот же DDL автоматически при старте (SCHEMA в storage.py, -- сид общей доски — boards_seed_shared()); файл нужен для ручного применения -- и как канонический список объектов. -- -- Применить вручную: -- docker compose exec -T db psql -U neon -d neon_crm \ -- -v ON_ERROR_STOP=1 < crm/migrate/notes_boards.sql -- -- Откат (теряются все заметки и история!): -- docker compose exec -T db psql -U neon -d neon_crm \ -- -c "DROP TABLE IF EXISTS note_activity, notes, board_members, boards CASCADE" -- -- Отступление от исходного ТЗ: пользователи в этой базе — crm_users с текстовым -- PK login (целочисленного users.id не существует), поэтому все ссылки идут -- на crm_users(login). -- ============================================================================ CREATE TABLE IF NOT EXISTS boards ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('shared', 'private')), title TEXT NOT NULL, created_by_login TEXT NOT NULL REFERENCES crm_users(login), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- участники приватных досок; для kind='shared' не используется — -- общая доска доступна всем ролям без проверки CREATE TABLE IF NOT EXISTS board_members ( board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE, user_login TEXT NOT NULL REFERENCES crm_users(login) ON DELETE CASCADE, PRIMARY KEY (board_id, user_login) ); -- заметка = задача: заголовок, текст, исполнитель, срок, «важно», статус. -- К сменам не привязана — доступна в любое время по факту логина. -- Логины автора/исполнителя без FK (как orders.waiter в storage.py): -- удаление сотрудника не должно ломать его задачи. CREATE TABLE IF NOT EXISTS notes ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE, author_login TEXT NOT NULL, assignee_login TEXT, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'done')), important BOOLEAN NOT NULL DEFAULT FALSE, due_date DATE, done_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_notes_board ON notes (board_id, status); CREATE INDEX IF NOT EXISTS idx_notes_assignee ON notes (assignee_login) WHERE status <> 'done'; -- журнал действий: created/status/assigned/edited/deleted/board_created. -- details: для assigned — логин исполнителя, для deleted — id заметки. -- note_id REFERENCES ... ON DELETE SET NULL: сама запись переживает удаление -- заметки, id удалённой сохраняется в details (для синхронизации клиентов). -- user_login без FK — журнал переживает удаление сотрудника. CREATE TABLE IF NOT EXISTS note_activity ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE, note_id BIGINT REFERENCES notes(id) ON DELETE SET NULL, user_login TEXT NOT NULL, action TEXT NOT NULL, details TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- одна общая доска на базу; владелец = первый по логину owner (база после -- seed CRM уже содержит учётки). Повторный запуск ничего не добавляет. INSERT INTO boards (kind, title, created_by_login) SELECT 'shared', 'Общая', u.login FROM crm_users u WHERE u.role = 'owner' AND NOT EXISTS (SELECT 1 FROM boards WHERE kind = 'shared') ORDER BY u.login LIMIT 1;