Add Feedback domain module: public submission flow, admin CRUD, reporting (#7)
Jenkins Production Deployment
Jenkins Production Deployment
Co-authored-by: Patrick Müller <mail@pmueller.me> Reviewed-on: #7 Co-authored-by: Patrick Mueller <patrick@mueller-patrick.tech> Co-committed-by: Patrick Mueller <patrick@mueller-patrick.tech>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
-- Nachklang e.V. Feedback module — initial schema for FEEDBACK_DB
|
||||
-- Apply manually against the FEEDBACK_DB database (separate from CALENDAR_DB).
|
||||
-- See nachklang-feedback/IMPLEMENTATION_PLAN.md §2 for the full rationale
|
||||
-- behind every design decision below (snapshot columns, denormalisation,
|
||||
-- absence-over-sentinels, hashed IPs only).
|
||||
--
|
||||
-- Apply with e.g.:
|
||||
-- mysql -h <DB_HOST> -u <DB_USER> -p <FEEDBACK_DB> < 001_init.sql
|
||||
--
|
||||
-- Deliberately no USE statement here: the target database is selected via
|
||||
-- the mysql command line above (whatever FEEDBACK_DB is actually named in
|
||||
-- .env), not hardcoded to a literal schema name.
|
||||
|
||||
-- 1. events -------------------------------------------------------------
|
||||
CREATE TABLE events (
|
||||
event_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(80) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
subtitle VARCHAR(255) NULL,
|
||||
event_date DATE NOT NULL,
|
||||
feedback_deadline DATETIME NOT NULL,
|
||||
is_published TINYINT(1) NOT NULL DEFAULT 0,
|
||||
intro_text TEXT NULL,
|
||||
created_by_email VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_events_slug (slug),
|
||||
KEY idx_events_eligibility (is_published, event_date, feedback_deadline)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. songs (per-event setlist) ------------------------------------------
|
||||
CREATE TABLE songs (
|
||||
song_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
composer VARCHAR(255) NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_songs_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
KEY idx_songs_event_position (event_id, position)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. questions (global reusable library) ---------------------------------
|
||||
CREATE TABLE questions (
|
||||
question_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
label VARCHAR(500) NOT NULL,
|
||||
help_text VARCHAR(500) NULL,
|
||||
question_type ENUM('SONG_PICK','SONG_RATING','FREE_TEXT') NOT NULL,
|
||||
is_archived TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_questions_archived_type (is_archived, question_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. event_questions (join + ordering) -----------------------------------
|
||||
CREATE TABLE event_questions (
|
||||
event_question_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
question_id INT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eq_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eq_question FOREIGN KEY (question_id) REFERENCES questions(question_id) ON DELETE RESTRICT,
|
||||
UNIQUE KEY uq_eq_event_question (event_id, question_id),
|
||||
KEY idx_eq_event_position (event_id, position, is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. submissions ----------------------------------------------------------
|
||||
CREATE TABLE submissions (
|
||||
submission_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
event_id INT NOT NULL,
|
||||
submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ip_hash CHAR(64) NULL,
|
||||
has_guestbook TINYINT(1) NOT NULL DEFAULT 0,
|
||||
has_newsletter TINYINT(1) NOT NULL DEFAULT 0,
|
||||
CONSTRAINT fk_sub_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
KEY idx_sub_event_time (event_id, submitted_at),
|
||||
KEY idx_sub_iphash_time (ip_hash, submitted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 6. submission_answers ----------------------------------------------------
|
||||
-- Deliberate denormalisation: question label/type and song title are
|
||||
-- snapshotted at submission time so later edits to the question library
|
||||
-- never retroactively change what a past submission means.
|
||||
CREATE TABLE submission_answers (
|
||||
answer_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
submission_id INT NOT NULL,
|
||||
event_id INT NOT NULL,
|
||||
question_id INT NULL,
|
||||
event_question_id INT NULL,
|
||||
question_label_snapshot VARCHAR(500) NOT NULL,
|
||||
question_type ENUM('SONG_PICK','SONG_RATING','FREE_TEXT') NOT NULL,
|
||||
position_snapshot INT NOT NULL DEFAULT 0,
|
||||
song_id INT NULL,
|
||||
song_title_snapshot VARCHAR(255) NULL,
|
||||
rating TINYINT NULL,
|
||||
text_answer TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_ans_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ans_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ans_question FOREIGN KEY (question_id) REFERENCES questions(question_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_ans_song FOREIGN KEY (song_id) REFERENCES songs(song_id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_ans_rating CHECK (rating IS NULL OR (rating BETWEEN 1 AND 5)),
|
||||
KEY idx_ans_submission (submission_id),
|
||||
KEY idx_ans_report (event_id, question_id, song_id),
|
||||
KEY idx_ans_type (event_id, question_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 7. guest_book_entries -----------------------------------------------------
|
||||
-- Private, admin-only. No public wall in v1.
|
||||
CREATE TABLE guest_book_entries (
|
||||
entry_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
submission_id INT NOT NULL,
|
||||
event_id INT NOT NULL,
|
||||
display_name VARCHAR(255) NULL,
|
||||
message TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_gb_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gb_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
KEY idx_gb_event_time (event_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 8. newsletter_signups -----------------------------------------------------
|
||||
CREATE TABLE newsletter_signups (
|
||||
signup_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
submission_id INT NOT NULL,
|
||||
event_id INT NOT NULL,
|
||||
first_name VARCHAR(120) NOT NULL,
|
||||
last_name VARCHAR(120) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
consent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
consent_text_version VARCHAR(40) NOT NULL,
|
||||
sync_status ENUM('PENDING','SENT','FAILED','SKIPPED') NOT NULL DEFAULT 'PENDING',
|
||||
sync_attempts INT NOT NULL DEFAULT 0,
|
||||
synced_at DATETIME NULL,
|
||||
external_id VARCHAR(120) NULL,
|
||||
last_error TEXT NULL,
|
||||
CONSTRAINT fk_nl_submission FOREIGN KEY (submission_id) REFERENCES submissions(submission_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_nl_event FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE,
|
||||
KEY idx_nl_sync_status (sync_status),
|
||||
KEY idx_nl_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Nachklang e.V. Feedback module — adds a per-event poster image URL.
|
||||
-- Apply manually against the FEEDBACK_DB database, after 001_init.sql:
|
||||
-- mysql -h <DB_HOST> -u <DB_USER> -p <FEEDBACK_DB> < 002_add_poster_image_url.sql
|
||||
--
|
||||
-- Stores a URL only (e.g. an existing nachklang.art poster image) rather
|
||||
-- than an uploaded file — the concert posters already live on the public
|
||||
-- website, so there is no need for the feedback app to host its own copy.
|
||||
ALTER TABLE events
|
||||
ADD COLUMN poster_image_url VARCHAR(500) NULL AFTER intro_text;
|
||||
Reference in New Issue
Block a user