From 46e490c04678ad02f10a75d2cb530a97b77ebea6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:59:25 +0000 Subject: [PATCH 01/24] Initial plan From 2602567a35433b072777c2a6378aeb8955788c46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:06:42 +0000 Subject: [PATCH 02/24] Add V2_11_0 Flyway migration for stewarding module Create stewarding tables for both PostgreSQL and SQLite: - stewarding_track, stewarding_penalty_catalog, stewarding_penalty_definition - stewarding_reasoning_template, stewarding_race_weekend, stewarding_session - stewarding_entrylist, stewarding_entrylist_entry, stewarding_entrylist_driver - stewarding_incident, stewarding_incident_involved_entry - stewarding_decision, stewarding_appeal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../postgres/V2_11_0__stewarding.sql | 197 ++++++++++++++++++ .../migration/sqlite/V2_11_0__stewarding.sql | 197 ++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql create mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql new file mode 100644 index 00000000..19788d18 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql @@ -0,0 +1,197 @@ +-- Stewarding module tables + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_track +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + country VARCHAR(100), + map_image_url VARCHAR(500), + map_metadata TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_penalty_catalog +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_penalty_definition +( + id SERIAL PRIMARY KEY, + catalog_id INTEGER NOT NULL, + code VARCHAR(50), + name VARCHAR(255) NOT NULL, + description TEXT, + category VARCHAR(100), + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + default_penalty VARCHAR(255), + severity INTEGER, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON simdesk.stewarding_penalty_definition (catalog_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_reasoning_template +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_race_weekend +( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + track_id INTEGER NOT NULL, + penalty_catalog_id INTEGER NOT NULL, + discord_webhook_url VARCHAR(500), + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (track_id) REFERENCES simdesk.stewarding_track (id), + FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_race_weekend_track_id ON simdesk.stewarding_race_weekend (track_id); +CREATE INDEX ix_stewarding_race_weekend_penalty_catalog_id ON simdesk.stewarding_race_weekend (penalty_catalog_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_session +( + id SERIAL PRIMARY KEY, + race_weekend_id INTEGER NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (race_weekend_id) REFERENCES simdesk.stewarding_race_weekend (id) +); + +CREATE INDEX ix_stewarding_session_race_weekend_id ON simdesk.stewarding_session (race_weekend_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist +( + id SERIAL PRIMARY KEY, + race_weekend_id INTEGER NOT NULL UNIQUE, + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (race_weekend_id) REFERENCES simdesk.stewarding_race_weekend (id) +); + +CREATE INDEX ix_stewarding_entrylist_race_weekend_id ON simdesk.stewarding_entrylist (race_weekend_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist_entry +( + id SERIAL PRIMARY KEY, + entrylist_id INTEGER NOT NULL, + race_number INTEGER NOT NULL, + car_model_id INTEGER, + team_name VARCHAR(255), + display_name VARCHAR(255), + FOREIGN KEY (entrylist_id) REFERENCES simdesk.stewarding_entrylist (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON simdesk.stewarding_entrylist_entry (entrylist_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist_driver +( + id SERIAL PRIMARY KEY, + entry_id INTEGER NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + short_name VARCHAR(10), + steam_id VARCHAR(50), + category INTEGER, + FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON simdesk.stewarding_entrylist_driver (entry_id); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident +( + id SERIAL PRIMARY KEY, + session_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + lap INTEGER, + timestamp_in_session VARCHAR(100), + map_marker_x DOUBLE PRECISION, + map_marker_y DOUBLE PRECISION, + video_url VARCHAR(500), + involved_cars_text VARCHAR(500), + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + reported_by_user_id INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id) +); + +CREATE INDEX ix_stewarding_incident_session_id ON simdesk.stewarding_incident (session_id); +CREATE INDEX ix_stewarding_incident_status ON simdesk.stewarding_incident (status); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident_involved_entry +( + incident_id INTEGER NOT NULL, + entry_id INTEGER NOT NULL, + PRIMARY KEY (incident_id, entry_id), + FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id) ON DELETE CASCADE, + FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_decision +( + id SERIAL PRIMARY KEY, + incident_id INTEGER, + session_id INTEGER NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id INTEGER, + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id INTEGER, + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id INTEGER, + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id INTEGER, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id), + FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id), + FOREIGN KEY (penalty_definition_id) REFERENCES simdesk.stewarding_penalty_definition (id), + FOREIGN KEY (reasoning_template_id) REFERENCES simdesk.stewarding_reasoning_template (id), + FOREIGN KEY (penalized_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id), + FOREIGN KEY (superseded_by_id) REFERENCES simdesk.stewarding_decision (id) +); + +CREATE INDEX ix_stewarding_decision_incident_id ON simdesk.stewarding_decision (incident_id); +CREATE INDEX ix_stewarding_decision_session_id ON simdesk.stewarding_decision (session_id); +CREATE INDEX ix_stewarding_decision_is_active ON simdesk.stewarding_decision (is_active); + +CREATE TABLE IF NOT EXISTS simdesk.stewarding_appeal +( + id SERIAL PRIMARY KEY, + decision_id INTEGER NOT NULL, + filed_by_user_id INTEGER, + filed_by_entry_id INTEGER, + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + response TEXT, + responded_by_user_id INTEGER, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + responded_at TIMESTAMP, + FOREIGN KEY (decision_id) REFERENCES simdesk.stewarding_decision (id), + FOREIGN KEY (filed_by_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) +); + +CREATE INDEX ix_stewarding_appeal_decision_id ON simdesk.stewarding_appeal (decision_id); +CREATE INDEX ix_stewarding_appeal_status ON simdesk.stewarding_appeal (status); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql new file mode 100644 index 00000000..58a52070 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql @@ -0,0 +1,197 @@ +-- Stewarding module tables + +CREATE TABLE IF NOT EXISTS stewarding_track +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + country VARCHAR(100), + map_image_url VARCHAR(500), + map_metadata TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS stewarding_penalty_catalog +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS stewarding_penalty_definition +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + catalog_id INTEGER NOT NULL, + code VARCHAR(50), + name VARCHAR(255) NOT NULL, + description TEXT, + category VARCHAR(100), + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + default_penalty VARCHAR(255), + severity INTEGER, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (catalog_id) REFERENCES stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON stewarding_penalty_definition (catalog_id); + +CREATE TABLE IF NOT EXISTS stewarding_reasoning_template +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS stewarding_race_weekend +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + description TEXT, + track_id INTEGER NOT NULL, + penalty_catalog_id INTEGER NOT NULL, + discord_webhook_url VARCHAR(500), + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (track_id) REFERENCES stewarding_track (id), + FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_race_weekend_track_id ON stewarding_race_weekend (track_id); +CREATE INDEX ix_stewarding_race_weekend_penalty_catalog_id ON stewarding_race_weekend (penalty_catalog_id); + +CREATE TABLE IF NOT EXISTS stewarding_session +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + race_weekend_id INTEGER NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (race_weekend_id) REFERENCES stewarding_race_weekend (id) +); + +CREATE INDEX ix_stewarding_session_race_weekend_id ON stewarding_session (race_weekend_id); + +CREATE TABLE IF NOT EXISTS stewarding_entrylist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + race_weekend_id INTEGER NOT NULL UNIQUE, + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (race_weekend_id) REFERENCES stewarding_race_weekend (id) +); + +CREATE INDEX ix_stewarding_entrylist_race_weekend_id ON stewarding_entrylist (race_weekend_id); + +CREATE TABLE IF NOT EXISTS stewarding_entrylist_entry +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entrylist_id INTEGER NOT NULL, + race_number INTEGER NOT NULL, + car_model_id INTEGER, + team_name VARCHAR(255), + display_name VARCHAR(255), + FOREIGN KEY (entrylist_id) REFERENCES stewarding_entrylist (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON stewarding_entrylist_entry (entrylist_id); + +CREATE TABLE IF NOT EXISTS stewarding_entrylist_driver +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entry_id INTEGER NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + short_name VARCHAR(10), + steam_id VARCHAR(50), + category INTEGER, + FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON stewarding_entrylist_driver (entry_id); + +CREATE TABLE IF NOT EXISTS stewarding_incident +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + lap INTEGER, + timestamp_in_session VARCHAR(100), + map_marker_x REAL, + map_marker_y REAL, + video_url VARCHAR(500), + involved_cars_text VARCHAR(500), + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + reported_by_user_id INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES stewarding_session (id) +); + +CREATE INDEX ix_stewarding_incident_session_id ON stewarding_incident (session_id); +CREATE INDEX ix_stewarding_incident_status ON stewarding_incident (status); + +CREATE TABLE IF NOT EXISTS stewarding_incident_involved_entry +( + incident_id INTEGER NOT NULL, + entry_id INTEGER NOT NULL, + PRIMARY KEY (incident_id, entry_id), + FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id) ON DELETE CASCADE, + FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS stewarding_decision +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + incident_id INTEGER, + session_id INTEGER NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id INTEGER, + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id INTEGER, + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id INTEGER, + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id INTEGER, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id), + FOREIGN KEY (session_id) REFERENCES stewarding_session (id), + FOREIGN KEY (penalty_definition_id) REFERENCES stewarding_penalty_definition (id), + FOREIGN KEY (reasoning_template_id) REFERENCES stewarding_reasoning_template (id), + FOREIGN KEY (penalized_entry_id) REFERENCES stewarding_entrylist_entry (id), + FOREIGN KEY (superseded_by_id) REFERENCES stewarding_decision (id) +); + +CREATE INDEX ix_stewarding_decision_incident_id ON stewarding_decision (incident_id); +CREATE INDEX ix_stewarding_decision_session_id ON stewarding_decision (session_id); +CREATE INDEX ix_stewarding_decision_is_active ON stewarding_decision (is_active); + +CREATE TABLE IF NOT EXISTS stewarding_appeal +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + decision_id INTEGER NOT NULL, + filed_by_user_id INTEGER, + filed_by_entry_id INTEGER, + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + response TEXT, + responded_by_user_id INTEGER, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + responded_at TIMESTAMP, + FOREIGN KEY (decision_id) REFERENCES stewarding_decision (id), + FOREIGN KEY (filed_by_entry_id) REFERENCES stewarding_entrylist_entry (id) +); + +CREATE INDEX ix_stewarding_appeal_decision_id ON stewarding_appeal (decision_id); +CREATE INDEX ix_stewarding_appeal_status ON stewarding_appeal (status); From 25c5e9d5eae6a025081b1afcf029fe3ad073607a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:09:25 +0000 Subject: [PATCH 03/24] Add stewarding entity classes and enums Create entity classes and enums for the SimRacing Stewarding suite under entities/stewarding/ package: Enums: IncidentStatus, AppealStatus, PenaltySessionType, StewSessionType Entities: StewardingTrack, PenaltyCatalog, PenaltyDefinition, RaceWeekend, RaceWeekendSession, StewardingEntrylist, StewardingEntrylistEntry, StewardingEntrylistDriver, Incident, StewardDecision, Appeal, ReasoningTemplate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../simdesk/entities/stewarding/Appeal.java | 26 ++++++++++++++ .../entities/stewarding/AppealStatus.java | 14 ++++++++ .../simdesk/entities/stewarding/Incident.java | 30 ++++++++++++++++ .../entities/stewarding/IncidentStatus.java | 17 ++++++++++ .../entities/stewarding/PenaltyCatalog.java | 21 ++++++++++++ .../stewarding/PenaltyDefinition.java | 24 +++++++++++++ .../stewarding/PenaltySessionType.java | 15 ++++++++ .../entities/stewarding/RaceWeekend.java | 34 +++++++++++++++++++ .../stewarding/RaceWeekendSession.java | 24 +++++++++++++ .../stewarding/ReasoningTemplate.java | 19 +++++++++++ .../entities/stewarding/StewSessionType.java | 14 ++++++++ .../entities/stewarding/StewardDecision.java | 30 ++++++++++++++++ .../stewarding/StewardingEntrylist.java | 20 +++++++++++ .../stewarding/StewardingEntrylistDriver.java | 21 ++++++++++++ .../stewarding/StewardingEntrylistEntry.java | 20 +++++++++++ .../entities/stewarding/StewardingTrack.java | 23 +++++++++++++ 16 files changed, 352 insertions(+) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/AppealStatus.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/IncidentStatus.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltySessionType.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewSessionType.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java new file mode 100644 index 00000000..77a31a76 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java @@ -0,0 +1,26 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Appeal { + private Integer id; + private Integer decisionId; + private Integer filedByUserId; + private Integer filedByEntryId; + private String reason; + private AppealStatus status; + private String response; + private Integer respondedByUserId; + private Instant filedAt; + private Instant respondedAt; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/AppealStatus.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/AppealStatus.java new file mode 100644 index 00000000..aeba339f --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/AppealStatus.java @@ -0,0 +1,14 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AppealStatus { + PENDING("Pending"), + ACCEPTED("Accepted"), + REJECTED("Rejected"); + + private final String description; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java new file mode 100644 index 00000000..5093b3eb --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java @@ -0,0 +1,30 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Incident { + private Integer id; + private Integer sessionId; + private String title; + private String description; + private Integer lap; + private String timestampInSession; + private Double mapMarkerX; + private Double mapMarkerY; + private String videoUrl; + private String involvedCarsText; + private IncidentStatus status; + private Integer reportedByUserId; + private Instant createdAt; + private Instant updatedAt; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/IncidentStatus.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/IncidentStatus.java new file mode 100644 index 00000000..4cc4d54d --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/IncidentStatus.java @@ -0,0 +1,17 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum IncidentStatus { + REPORTED("Reported"), + UNDER_REVIEW("Under Review"), + DECISION_MADE("Decision Made"), + APPEALED("Appealed"), + APPEAL_REVIEWED("Appeal Reviewed"), + CLOSED("Closed"); + + private final String description; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java new file mode 100644 index 00000000..9f7f0452 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java @@ -0,0 +1,21 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PenaltyCatalog { + private Integer id; + private String name; + private String description; + private Instant createdAt; + private Instant updatedAt; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java new file mode 100644 index 00000000..439d2f44 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java @@ -0,0 +1,24 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PenaltyDefinition { + private Integer id; + private Integer catalogId; + private String code; + private String name; + private String description; + private String category; + private PenaltySessionType sessionType; + private String defaultPenalty; + private Integer severity; + private Integer sortOrder; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltySessionType.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltySessionType.java new file mode 100644 index 00000000..ee2ae3b9 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltySessionType.java @@ -0,0 +1,15 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PenaltySessionType { + PRACTICE("Practice"), + QUALIFYING("Qualifying"), + RACE("Race"), + ALL("All"); + + private final String description; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java new file mode 100644 index 00000000..87fb6c3f --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java @@ -0,0 +1,34 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RaceWeekend { + private Integer id; + private String title; + private String description; + private Integer trackId; + private Integer penaltyCatalogId; + private String discordWebhookUrl; + private LocalDate startDate; + private LocalDate endDate; + private Instant createdAt; + private Instant updatedAt; + + @EqualsAndHashCode.Exclude + private StewardingTrack track; + + @EqualsAndHashCode.Exclude + private PenaltyCatalog penaltyCatalog; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java new file mode 100644 index 00000000..1f908bf5 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java @@ -0,0 +1,24 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RaceWeekendSession { + private Integer id; + private Integer raceWeekendId; + private StewSessionType sessionType; + private String title; + private Instant startTime; + private Instant endTime; + private Integer sortOrder; + private Instant createdAt; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java new file mode 100644 index 00000000..452a447b --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java @@ -0,0 +1,19 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ReasoningTemplate { + private Integer id; + private String name; + private String category; + private String templateText; + private Integer sortOrder; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewSessionType.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewSessionType.java new file mode 100644 index 00000000..2993eae0 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewSessionType.java @@ -0,0 +1,14 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum StewSessionType { + PRACTICE("Practice"), + QUALIFYING("Qualifying"), + RACE("Race"); + + private final String description; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java new file mode 100644 index 00000000..7b404079 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java @@ -0,0 +1,30 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StewardDecision { + private Integer id; + private Integer incidentId; + private Integer sessionId; + private Integer decidedByUserId; + private Integer penaltyDefinitionId; + private String customPenalty; + private String reasoning; + private Integer reasoningTemplateId; + private Boolean isNoAction; + private Integer penalizedEntryId; + private String penalizedCarText; + private Instant decidedAt; + private Integer supersededById; + private Boolean isActive; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java new file mode 100644 index 00000000..345f42e6 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java @@ -0,0 +1,20 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StewardingEntrylist { + private Integer id; + private Integer raceWeekendId; + private Instant uploadedAt; + private String rawJson; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java new file mode 100644 index 00000000..2dc7c77a --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java @@ -0,0 +1,21 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StewardingEntrylistDriver { + private Integer id; + private Integer entryId; + private String firstName; + private String lastName; + private String shortName; + private String steamId; + private Integer category; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java new file mode 100644 index 00000000..ecbd0d63 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java @@ -0,0 +1,20 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StewardingEntrylistEntry { + private Integer id; + private Integer entrylistId; + private Integer raceNumber; + private Integer carModelId; + private String teamName; + private String displayName; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java new file mode 100644 index 00000000..2d8683a0 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java @@ -0,0 +1,23 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StewardingTrack { + private Integer id; + private String name; + private String country; + private String mapImageUrl; + private String mapMetadata; + private Instant createdAt; + private Instant updatedAt; +} From ebf18bec7de420da91ebb670eec969ac497e48cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:14:41 +0000 Subject: [PATCH 04/24] Add MyBatis mapper interfaces for SimRacing Stewarding suite Create 13 annotation-based MyBatis mapper interfaces for the stewarding domain: StewardingTrackMapper, PenaltyCatalogMapper, PenaltyDefinitionMapper, RaceWeekendMapper, RaceWeekendSessionMapper, StewardingEntrylistMapper, StewardingEntrylistEntryMapper, StewardingEntrylistDriverMapper, StewardingIncidentMapper, StewardingIncidentInvolvedEntryMapper, StewardDecisionMapper, StewardingAppealMapper, and ReasoningTemplateMapper. All mappers follow the existing project pattern using @Component, @Mapper, @Results/@ResultMap for column mapping, and annotation-based SQL with @Select, @Insert, @Update, @Delete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../mybatis/mapper/PenaltyCatalogMapper.java | 42 +++++++++++++ .../mapper/PenaltyDefinitionMapper.java | 60 ++++++++++++++++++ .../mybatis/mapper/RaceWeekendMapper.java | 52 ++++++++++++++++ .../mapper/RaceWeekendSessionMapper.java | 46 ++++++++++++++ .../mapper/ReasoningTemplateMapper.java | 46 ++++++++++++++ .../mybatis/mapper/StewardDecisionMapper.java | 61 +++++++++++++++++++ .../mapper/StewardingAppealMapper.java | 44 +++++++++++++ .../StewardingEntrylistDriverMapper.java | 37 +++++++++++ .../StewardingEntrylistEntryMapper.java | 36 +++++++++++ .../mapper/StewardingEntrylistMapper.java | 34 +++++++++++ ...StewardingIncidentInvolvedEntryMapper.java | 22 +++++++ .../mapper/StewardingIncidentMapper.java | 59 ++++++++++++++++++ .../mybatis/mapper/StewardingTrackMapper.java | 44 +++++++++++++ 13 files changed, 583 insertions(+) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java new file mode 100644 index 00000000..9de3c234 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java @@ -0,0 +1,42 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface PenaltyCatalogMapper { + @Results(id = "penaltyCatalogResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "name", column = "name"), + @Result(property = "description", column = "description"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_penalty_catalog ORDER BY name") + List findAll(); + + @ResultMap("penaltyCatalogResultMap") + @Select("SELECT * FROM stewarding_penalty_catalog WHERE id = #{id}") + PenaltyCatalog findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_penalty_catalog (name, description, created_at, updated_at) + VALUES (#{name}, #{description}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(PenaltyCatalog catalog); + + @Update(""" + UPDATE stewarding_penalty_catalog + SET name = #{name}, description = #{description}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(PenaltyCatalog catalog); + + @Delete("DELETE FROM stewarding_penalty_catalog WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java new file mode 100644 index 00000000..08498a7d --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java @@ -0,0 +1,60 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface PenaltyDefinitionMapper { + @Results(id = "penaltyDefinitionResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "catalogId", column = "catalog_id"), + @Result(property = "code", column = "code"), + @Result(property = "name", column = "name"), + @Result(property = "description", column = "description"), + @Result(property = "category", column = "category"), + @Result(property = "sessionType", column = "session_type"), + @Result(property = "defaultPenalty", column = "default_penalty"), + @Result(property = "severity", column = "severity"), + @Result(property = "sortOrder", column = "sort_order"), + }) + @Select("SELECT * FROM stewarding_penalty_definition WHERE catalog_id = #{catalogId} ORDER BY sort_order, name") + List findByCatalogId(Integer catalogId); + + @ResultMap("penaltyDefinitionResultMap") + @Select("SELECT * FROM stewarding_penalty_definition WHERE id = #{id}") + PenaltyDefinition findById(Integer id); + + @ResultMap("penaltyDefinitionResultMap") + @Select(""" + SELECT * FROM stewarding_penalty_definition + WHERE catalog_id = #{catalogId} AND (session_type = #{sessionType} OR session_type = 'ALL') + ORDER BY sort_order, name + """) + List findByCatalogIdAndSessionType(Integer catalogId, String sessionType); + + @ResultMap("penaltyDefinitionResultMap") + @Select("SELECT * FROM stewarding_penalty_definition WHERE catalog_id = #{catalogId} ORDER BY category, sort_order") + List findByCatalogIdGroupedByCategory(Integer catalogId); + + @Insert(""" + INSERT INTO stewarding_penalty_definition (catalog_id, code, name, description, category, session_type, default_penalty, severity, sort_order) + VALUES (#{catalogId}, #{code}, #{name}, #{description}, #{category}, #{sessionType}, #{defaultPenalty}, #{severity}, #{sortOrder}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(PenaltyDefinition definition); + + @Update(""" + UPDATE stewarding_penalty_definition + SET catalog_id = #{catalogId}, code = #{code}, name = #{name}, description = #{description}, category = #{category}, + session_type = #{sessionType}, default_penalty = #{defaultPenalty}, severity = #{severity}, sort_order = #{sortOrder} + WHERE id = #{id} + """) + void update(PenaltyDefinition definition); + + @Delete("DELETE FROM stewarding_penalty_definition WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java new file mode 100644 index 00000000..8f8cf070 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java @@ -0,0 +1,52 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface RaceWeekendMapper { + @Results(id = "raceWeekendResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "title", column = "title"), + @Result(property = "description", column = "description"), + @Result(property = "trackId", column = "track_id"), + @Result(property = "penaltyCatalogId", column = "penalty_catalog_id"), + @Result(property = "discordWebhookUrl", column = "discord_webhook_url"), + @Result(property = "startDate", column = "start_date"), + @Result(property = "endDate", column = "end_date"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_race_weekend ORDER BY start_date DESC") + List findAll(); + + @ResultMap("raceWeekendResultMap") + @Select("SELECT * FROM stewarding_race_weekend WHERE id = #{id}") + RaceWeekend findById(Integer id); + + @ResultMap("raceWeekendResultMap") + @Select("SELECT * FROM stewarding_race_weekend WHERE track_id = #{trackId}") + List findByTrackId(Integer trackId); + + @Insert(""" + INSERT INTO stewarding_race_weekend (title, description, track_id, penalty_catalog_id, discord_webhook_url, start_date, end_date, created_at, updated_at) + VALUES (#{title}, #{description}, #{trackId}, #{penaltyCatalogId}, #{discordWebhookUrl}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(RaceWeekend weekend); + + @Update(""" + UPDATE stewarding_race_weekend + SET title = #{title}, description = #{description}, track_id = #{trackId}, penalty_catalog_id = #{penaltyCatalogId}, + discord_webhook_url = #{discordWebhookUrl}, start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(RaceWeekend weekend); + + @Delete("DELETE FROM stewarding_race_weekend WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java new file mode 100644 index 00000000..1d6264db --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java @@ -0,0 +1,46 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface RaceWeekendSessionMapper { + @Results(id = "raceWeekendSessionResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "raceWeekendId", column = "race_weekend_id"), + @Result(property = "sessionType", column = "session_type"), + @Result(property = "title", column = "title"), + @Result(property = "startTime", column = "start_time"), + @Result(property = "endTime", column = "end_time"), + @Result(property = "sortOrder", column = "sort_order"), + @Result(property = "createdAt", column = "created_at"), + }) + @Select("SELECT * FROM stewarding_session WHERE race_weekend_id = #{raceWeekendId} ORDER BY sort_order") + List findByRaceWeekendId(Integer raceWeekendId); + + @ResultMap("raceWeekendSessionResultMap") + @Select("SELECT * FROM stewarding_session WHERE id = #{id}") + RaceWeekendSession findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_session (race_weekend_id, session_type, title, start_time, end_time, sort_order, created_at) + VALUES (#{raceWeekendId}, #{sessionType}, #{title}, #{startTime}, #{endTime}, #{sortOrder}, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(RaceWeekendSession session); + + @Update(""" + UPDATE stewarding_session + SET race_weekend_id = #{raceWeekendId}, session_type = #{sessionType}, title = #{title}, + start_time = #{startTime}, end_time = #{endTime}, sort_order = #{sortOrder} + WHERE id = #{id} + """) + void update(RaceWeekendSession session); + + @Delete("DELETE FROM stewarding_session WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java new file mode 100644 index 00000000..3d8da233 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java @@ -0,0 +1,46 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface ReasoningTemplateMapper { + @Results(id = "reasoningTemplateResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "name", column = "name"), + @Result(property = "category", column = "category"), + @Result(property = "templateText", column = "template_text"), + @Result(property = "sortOrder", column = "sort_order"), + }) + @Select("SELECT * FROM stewarding_reasoning_template ORDER BY category, sort_order") + List findAll(); + + @ResultMap("reasoningTemplateResultMap") + @Select("SELECT * FROM stewarding_reasoning_template WHERE id = #{id}") + ReasoningTemplate findById(Integer id); + + @ResultMap("reasoningTemplateResultMap") + @Select("SELECT * FROM stewarding_reasoning_template WHERE category = #{category} ORDER BY sort_order") + List findByCategory(String category); + + @Insert(""" + INSERT INTO stewarding_reasoning_template (name, category, template_text, sort_order) + VALUES (#{name}, #{category}, #{templateText}, #{sortOrder}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(ReasoningTemplate template); + + @Update(""" + UPDATE stewarding_reasoning_template + SET name = #{name}, category = #{category}, template_text = #{templateText}, sort_order = #{sortOrder} + WHERE id = #{id} + """) + void update(ReasoningTemplate template); + + @Delete("DELETE FROM stewarding_reasoning_template WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java new file mode 100644 index 00000000..c02da68e --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java @@ -0,0 +1,61 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.StewardDecision; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardDecisionMapper { + @Results(id = "stewardDecisionResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "incidentId", column = "incident_id"), + @Result(property = "sessionId", column = "session_id"), + @Result(property = "decidedByUserId", column = "decided_by_user_id"), + @Result(property = "penaltyDefinitionId", column = "penalty_definition_id"), + @Result(property = "customPenalty", column = "custom_penalty"), + @Result(property = "reasoning", column = "reasoning"), + @Result(property = "reasoningTemplateId", column = "reasoning_template_id"), + @Result(property = "isNoAction", column = "is_no_action"), + @Result(property = "penalizedEntryId", column = "penalized_entry_id"), + @Result(property = "penalizedCarText", column = "penalized_car_text"), + @Result(property = "decidedAt", column = "decided_at"), + @Result(property = "supersededById", column = "superseded_by_id"), + @Result(property = "isActive", column = "is_active"), + }) + @Select("SELECT * FROM stewarding_decision WHERE id = #{id}") + StewardDecision findById(Integer id); + + @ResultMap("stewardDecisionResultMap") + @Select("SELECT * FROM stewarding_decision WHERE incident_id = #{incidentId} AND is_active = true") + List findActiveByIncidentId(Integer incidentId); + + @ResultMap("stewardDecisionResultMap") + @Select("SELECT * FROM stewarding_decision WHERE incident_id = #{incidentId} ORDER BY decided_at DESC") + List findByIncidentId(Integer incidentId); + + @ResultMap("stewardDecisionResultMap") + @Select("SELECT * FROM stewarding_decision WHERE session_id = #{sessionId} AND is_active = true ORDER BY decided_at DESC") + List findBySessionId(Integer sessionId); + + @ResultMap("stewardDecisionResultMap") + @Select("SELECT * FROM stewarding_decision WHERE incident_id IS NULL AND session_id = #{sessionId} AND is_active = true ORDER BY decided_at DESC") + List findManualBySessionId(Integer sessionId); + + @Insert(""" + INSERT INTO stewarding_decision (incident_id, session_id, decided_by_user_id, penalty_definition_id, custom_penalty, + reasoning, reasoning_template_id, is_no_action, penalized_entry_id, penalized_car_text, decided_at, superseded_by_id, is_active) + VALUES (#{incidentId}, #{sessionId}, #{decidedByUserId}, #{penaltyDefinitionId}, #{customPenalty}, + #{reasoning}, #{reasoningTemplateId}, #{isNoAction}, #{penalizedEntryId}, #{penalizedCarText}, CURRENT_TIMESTAMP, #{supersededById}, #{isActive}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(StewardDecision decision); + + @Update("UPDATE stewarding_decision SET is_active = false WHERE id = #{id}") + void deactivate(Integer id); + + @Update("UPDATE stewarding_decision SET superseded_by_id = #{supersededById} WHERE id = #{id}") + void setSupersededBy(Integer id, Integer supersededById); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java new file mode 100644 index 00000000..b83e5d9f --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java @@ -0,0 +1,44 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.Appeal; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingAppealMapper { + @Results(id = "stewardingAppealResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "decisionId", column = "decision_id"), + @Result(property = "filedByUserId", column = "filed_by_user_id"), + @Result(property = "filedByEntryId", column = "filed_by_entry_id"), + @Result(property = "reason", column = "reason"), + @Result(property = "status", column = "status"), + @Result(property = "response", column = "response"), + @Result(property = "respondedByUserId", column = "responded_by_user_id"), + @Result(property = "filedAt", column = "filed_at"), + @Result(property = "respondedAt", column = "responded_at"), + }) + @Select("SELECT * FROM stewarding_appeal WHERE decision_id = #{decisionId} ORDER BY filed_at DESC") + List findByDecisionId(Integer decisionId); + + @ResultMap("stewardingAppealResultMap") + @Select("SELECT * FROM stewarding_appeal WHERE id = #{id}") + Appeal findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_appeal (decision_id, filed_by_user_id, filed_by_entry_id, reason, status, filed_at) + VALUES (#{decisionId}, #{filedByUserId}, #{filedByEntryId}, #{reason}, #{status}, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(Appeal appeal); + + @Update(""" + UPDATE stewarding_appeal + SET status = #{status}, response = #{response}, responded_by_user_id = #{respondedByUserId}, responded_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void updateResponse(Integer id, String status, String response, Integer respondedByUserId); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java new file mode 100644 index 00000000..891e9480 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java @@ -0,0 +1,37 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistDriver; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingEntrylistDriverMapper { + @Results(id = "stewardingEntrylistDriverResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "entryId", column = "entry_id"), + @Result(property = "firstName", column = "first_name"), + @Result(property = "lastName", column = "last_name"), + @Result(property = "shortName", column = "short_name"), + @Result(property = "steamId", column = "steam_id"), + @Result(property = "category", column = "category"), + }) + @Select("SELECT * FROM stewarding_entrylist_driver WHERE entry_id = #{entryId}") + List findByEntryId(Integer entryId); + + @ResultMap("stewardingEntrylistDriverResultMap") + @Select("SELECT * FROM stewarding_entrylist_driver WHERE id = #{id}") + StewardingEntrylistDriver findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_entrylist_driver (entry_id, first_name, last_name, short_name, steam_id, category) + VALUES (#{entryId}, #{firstName}, #{lastName}, #{shortName}, #{steamId}, #{category}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(StewardingEntrylistDriver driver); + + @Delete("DELETE FROM stewarding_entrylist_driver WHERE entry_id = #{entryId}") + void deleteByEntryId(Integer entryId); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java new file mode 100644 index 00000000..2cd6f83a --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java @@ -0,0 +1,36 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistEntry; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingEntrylistEntryMapper { + @Results(id = "stewardingEntrylistEntryResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "entrylistId", column = "entrylist_id"), + @Result(property = "raceNumber", column = "race_number"), + @Result(property = "carModelId", column = "car_model_id"), + @Result(property = "teamName", column = "team_name"), + @Result(property = "displayName", column = "display_name"), + }) + @Select("SELECT * FROM stewarding_entrylist_entry WHERE entrylist_id = #{entrylistId} ORDER BY race_number") + List findByEntrylistId(Integer entrylistId); + + @ResultMap("stewardingEntrylistEntryResultMap") + @Select("SELECT * FROM stewarding_entrylist_entry WHERE id = #{id}") + StewardingEntrylistEntry findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_entrylist_entry (entrylist_id, race_number, car_model_id, team_name, display_name) + VALUES (#{entrylistId}, #{raceNumber}, #{carModelId}, #{teamName}, #{displayName}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(StewardingEntrylistEntry entry); + + @Delete("DELETE FROM stewarding_entrylist_entry WHERE entrylist_id = #{entrylistId}") + void deleteByEntrylistId(Integer entrylistId); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java new file mode 100644 index 00000000..9ca22dfb --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java @@ -0,0 +1,34 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylist; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingEntrylistMapper { + @Results(id = "stewardingEntrylistResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "raceWeekendId", column = "race_weekend_id"), + @Result(property = "uploadedAt", column = "uploaded_at"), + @Result(property = "rawJson", column = "raw_json"), + }) + @Select("SELECT * FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") + List findByRaceWeekendId(Integer raceWeekendId); + + @ResultMap("stewardingEntrylistResultMap") + @Select("SELECT * FROM stewarding_entrylist WHERE id = #{id}") + StewardingEntrylist findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_entrylist (race_weekend_id, uploaded_at, raw_json) + VALUES (#{raceWeekendId}, CURRENT_TIMESTAMP, #{rawJson}) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(StewardingEntrylist entrylist); + + @Delete("DELETE FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") + void deleteByRaceWeekendId(Integer raceWeekendId); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java new file mode 100644 index 00000000..70aaf586 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java @@ -0,0 +1,22 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingIncidentInvolvedEntryMapper { + @Select("SELECT entry_id FROM stewarding_incident_involved_entry WHERE incident_id = #{incidentId}") + List findEntryIdsByIncidentId(Integer incidentId); + + @Insert("INSERT INTO stewarding_incident_involved_entry (incident_id, entry_id) VALUES (#{incidentId}, #{entryId})") + void insert(Integer incidentId, Integer entryId); + + @Delete("DELETE FROM stewarding_incident_involved_entry WHERE incident_id = #{incidentId}") + void deleteByIncidentId(Integer incidentId); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java new file mode 100644 index 00000000..1d589adb --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java @@ -0,0 +1,59 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.Incident; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingIncidentMapper { + @Results(id = "stewardingIncidentResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "sessionId", column = "session_id"), + @Result(property = "title", column = "title"), + @Result(property = "description", column = "description"), + @Result(property = "lap", column = "lap"), + @Result(property = "timestampInSession", column = "timestamp_in_session"), + @Result(property = "mapMarkerX", column = "map_marker_x"), + @Result(property = "mapMarkerY", column = "map_marker_y"), + @Result(property = "videoUrl", column = "video_url"), + @Result(property = "involvedCarsText", column = "involved_cars_text"), + @Result(property = "status", column = "status"), + @Result(property = "reportedByUserId", column = "reported_by_user_id"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_incident WHERE session_id = #{sessionId} ORDER BY created_at DESC") + List findBySessionId(Integer sessionId); + + @ResultMap("stewardingIncidentResultMap") + @Select("SELECT * FROM stewarding_incident WHERE id = #{id}") + Incident findById(Integer id); + + @ResultMap("stewardingIncidentResultMap") + @Select("SELECT * FROM stewarding_incident WHERE session_id = #{sessionId} AND status = #{status} ORDER BY created_at DESC") + List findBySessionIdAndStatus(Integer sessionId, String status); + + @Insert(""" + INSERT INTO stewarding_incident (session_id, title, description, lap, timestamp_in_session, map_marker_x, map_marker_y, + video_url, involved_cars_text, status, reported_by_user_id, created_at, updated_at) + VALUES (#{sessionId}, #{title}, #{description}, #{lap}, #{timestampInSession}, #{mapMarkerX}, #{mapMarkerY}, + #{videoUrl}, #{involvedCarsText}, #{status}, #{reportedByUserId}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(Incident incident); + + @Update("UPDATE stewarding_incident SET status = #{status}, updated_at = CURRENT_TIMESTAMP WHERE id = #{id}") + void updateStatus(Integer id, String status); + + @Update(""" + UPDATE stewarding_incident + SET title = #{title}, description = #{description}, lap = #{lap}, timestamp_in_session = #{timestampInSession}, + map_marker_x = #{mapMarkerX}, map_marker_y = #{mapMarkerY}, video_url = #{videoUrl}, + involved_cars_text = #{involvedCarsText}, status = #{status}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(Incident incident); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java new file mode 100644 index 00000000..36847127 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java @@ -0,0 +1,44 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface StewardingTrackMapper { + @Results(id = "stewardingTrackResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "name", column = "name"), + @Result(property = "country", column = "country"), + @Result(property = "mapImageUrl", column = "map_image_url"), + @Result(property = "mapMetadata", column = "map_metadata"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_track ORDER BY name") + List findAll(); + + @ResultMap("stewardingTrackResultMap") + @Select("SELECT * FROM stewarding_track WHERE id = #{id}") + StewardingTrack findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_track (name, country, map_image_url, map_metadata, created_at, updated_at) + VALUES (#{name}, #{country}, #{mapImageUrl}, #{mapMetadata}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(StewardingTrack track); + + @Update(""" + UPDATE stewarding_track + SET name = #{name}, country = #{country}, map_image_url = #{mapImageUrl}, map_metadata = #{mapMetadata}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(StewardingTrack track); + + @Delete("DELETE FROM stewarding_track WHERE id = #{id}") + void delete(Integer id); +} From d7aada30de128d2e482c94c753a5040ebb99a0ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:21:16 +0000 Subject: [PATCH 05/24] Add service classes for SimRacing Stewarding suite Create 9 service classes in the stewarding package: - StewardingTrackService: CRUD operations for tracks - PenaltyCatalogService: CRUD for penalty catalogs and definitions - RaceWeekendService: CRUD for race weekends and sessions with eager loading - StewardingEntrylistService: Entrylist management with ACC JSON parsing - StewardingIncidentService: Incident management with involved entries - StewardDecisionService: Decision making and revision workflow - StewardingAppealService: Appeal filing and review workflow - ReasoningTemplateService: Template CRUD with variable rendering - StewardingDiscordNotificationService: Async Discord webhook notifications All services are gated behind @Profile(SpringProfile.STEWARDING) and follow existing project patterns with @RequiredArgsConstructor and @Transactional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../stewarding/PenaltyCatalogService.java | 67 +++++++ .../stewarding/RaceWeekendService.java | 80 +++++++++ .../stewarding/ReasoningTemplateService.java | 54 ++++++ .../stewarding/StewardDecisionService.java | 57 ++++++ .../stewarding/StewardingAppealService.java | 56 ++++++ .../StewardingDiscordNotificationService.java | 168 ++++++++++++++++++ .../StewardingEntrylistService.java | 107 +++++++++++ .../stewarding/StewardingIncidentService.java | 50 ++++++ .../stewarding/StewardingTrackService.java | 41 +++++ 9 files changed, 680 insertions(+) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java new file mode 100644 index 00000000..b48cad05 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java @@ -0,0 +1,67 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; +import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; +import de.sustineo.simdesk.mybatis.mapper.PenaltyDefinitionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class PenaltyCatalogService { + private final PenaltyCatalogMapper catalogMapper; + private final PenaltyDefinitionMapper definitionMapper; + + public List getAllCatalogs() { + return catalogMapper.findAll(); + } + + public PenaltyCatalog getCatalogById(Integer id) { + return catalogMapper.findById(id); + } + + @Transactional + public void createCatalog(PenaltyCatalog catalog) { + catalogMapper.insert(catalog); + } + + @Transactional + public void updateCatalog(PenaltyCatalog catalog) { + catalogMapper.update(catalog); + } + + @Transactional + public void deleteCatalog(Integer id) { + catalogMapper.delete(id); + } + + public List getDefinitionsByCatalogId(Integer catalogId) { + return definitionMapper.findByCatalogId(catalogId); + } + + public List getDefinitionsForSessionType(Integer catalogId, String sessionType) { + return definitionMapper.findByCatalogIdAndSessionType(catalogId, sessionType); + } + + @Transactional + public void createDefinition(PenaltyDefinition definition) { + definitionMapper.insert(definition); + } + + @Transactional + public void updateDefinition(PenaltyDefinition definition) { + definitionMapper.update(definition); + } + + @Transactional + public void deleteDefinition(Integer id) { + definitionMapper.delete(id); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java new file mode 100644 index 00000000..4908f04f --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java @@ -0,0 +1,80 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; +import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; +import de.sustineo.simdesk.mybatis.mapper.RaceWeekendMapper; +import de.sustineo.simdesk.mybatis.mapper.RaceWeekendSessionMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class RaceWeekendService { + private final RaceWeekendMapper weekendMapper; + private final RaceWeekendSessionMapper sessionMapper; + private final StewardingTrackMapper trackMapper; + private final PenaltyCatalogMapper catalogMapper; + + public List getAllWeekends() { + return weekendMapper.findAll(); + } + + public RaceWeekend getWeekendById(Integer id) { + RaceWeekend weekend = weekendMapper.findById(id); + if (weekend != null) { + if (weekend.getTrackId() != null) { + weekend.setTrack(trackMapper.findById(weekend.getTrackId())); + } + if (weekend.getPenaltyCatalogId() != null) { + weekend.setPenaltyCatalog(catalogMapper.findById(weekend.getPenaltyCatalogId())); + } + } + return weekend; + } + + @Transactional + public void createWeekend(RaceWeekend weekend) { + weekendMapper.insert(weekend); + } + + @Transactional + public void updateWeekend(RaceWeekend weekend) { + weekendMapper.update(weekend); + } + + @Transactional + public void deleteWeekend(Integer id) { + weekendMapper.delete(id); + } + + public List getSessionsByWeekendId(Integer weekendId) { + return sessionMapper.findByRaceWeekendId(weekendId); + } + + public RaceWeekendSession getSessionById(Integer id) { + return sessionMapper.findById(id); + } + + @Transactional + public void createSession(RaceWeekendSession session) { + sessionMapper.insert(session); + } + + @Transactional + public void updateSession(RaceWeekendSession session) { + sessionMapper.update(session); + } + + @Transactional + public void deleteSession(Integer id) { + sessionMapper.delete(id); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java new file mode 100644 index 00000000..1718b3fa --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java @@ -0,0 +1,54 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; +import de.sustineo.simdesk.mybatis.mapper.ReasoningTemplateMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class ReasoningTemplateService { + private final ReasoningTemplateMapper templateMapper; + + public List getAllTemplates() { + return templateMapper.findAll(); + } + + public ReasoningTemplate getTemplateById(Integer id) { + return templateMapper.findById(id); + } + + public List getTemplatesByCategory(String category) { + return templateMapper.findByCategory(category); + } + + @Transactional + public void createTemplate(ReasoningTemplate template) { + templateMapper.insert(template); + } + + @Transactional + public void updateTemplate(ReasoningTemplate template) { + templateMapper.update(template); + } + + @Transactional + public void deleteTemplate(Integer id) { + templateMapper.delete(id); + } + + public String renderTemplate(String templateText, Map variables) { + String result = templateText; + for (Map.Entry entry : variables.entrySet()) { + result = result.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return result; + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java new file mode 100644 index 00000000..b93a3416 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java @@ -0,0 +1,57 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.IncidentStatus; +import de.sustineo.simdesk.entities.stewarding.StewardDecision; +import de.sustineo.simdesk.mybatis.mapper.StewardDecisionMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class StewardDecisionService { + private final StewardDecisionMapper decisionMapper; + private final StewardingIncidentMapper incidentMapper; + + public StewardDecision getDecisionById(Integer id) { + return decisionMapper.findById(id); + } + + public StewardDecision getActiveDecisionByIncidentId(Integer incidentId) { + List decisions = decisionMapper.findActiveByIncidentId(incidentId); + return decisions.isEmpty() ? null : decisions.getFirst(); + } + + public List getDecisionHistory(Integer incidentId) { + return decisionMapper.findByIncidentId(incidentId); + } + + public List getDecisionsBySessionId(Integer sessionId) { + return decisionMapper.findBySessionId(sessionId); + } + + public List getManualDecisionsBySessionId(Integer sessionId) { + return decisionMapper.findManualBySessionId(sessionId); + } + + @Transactional + public void makeDecision(StewardDecision decision) { + decisionMapper.insert(decision); + if (decision.getIncidentId() != null) { + incidentMapper.updateStatus(decision.getIncidentId(), IncidentStatus.DECISION_MADE.name()); + } + } + + @Transactional + public void reviseDecision(Integer oldDecisionId, StewardDecision newDecision) { + decisionMapper.deactivate(oldDecisionId); + decisionMapper.insert(newDecision); + decisionMapper.setSupersededBy(oldDecisionId, newDecision.getId()); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java new file mode 100644 index 00000000..54338a94 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java @@ -0,0 +1,56 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.Appeal; +import de.sustineo.simdesk.entities.stewarding.AppealStatus; +import de.sustineo.simdesk.entities.stewarding.IncidentStatus; +import de.sustineo.simdesk.entities.stewarding.StewardDecision; +import de.sustineo.simdesk.mybatis.mapper.StewardDecisionMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingAppealMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class StewardingAppealService { + private final StewardingAppealMapper appealMapper; + private final StewardingIncidentMapper incidentMapper; + private final StewardDecisionMapper decisionMapper; + + public List getAppealsByDecisionId(Integer decisionId) { + return appealMapper.findByDecisionId(decisionId); + } + + public Appeal getAppealById(Integer id) { + return appealMapper.findById(id); + } + + @Transactional + public void fileAppeal(Appeal appeal) { + appealMapper.insert(appeal); + StewardDecision decision = decisionMapper.findById(appeal.getDecisionId()); + if (decision != null && decision.getIncidentId() != null) { + incidentMapper.updateStatus(decision.getIncidentId(), IncidentStatus.APPEALED.name()); + } + } + + @Transactional + public void reviewAppeal(Integer id, AppealStatus status, String response, Integer respondedByUserId) { + appealMapper.updateResponse(id, status.name(), response, respondedByUserId); + if (status == AppealStatus.ACCEPTED || status == AppealStatus.REJECTED) { + Appeal appeal = appealMapper.findById(id); + if (appeal != null) { + StewardDecision decision = decisionMapper.findById(appeal.getDecisionId()); + if (decision != null && decision.getIncidentId() != null) { + incidentMapper.updateStatus(decision.getIncidentId(), IncidentStatus.APPEAL_REVIEWED.name()); + } + } + } + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java new file mode 100644 index 00000000..08f94548 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java @@ -0,0 +1,168 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.*; +import de.sustineo.simdesk.mybatis.mapper.RaceWeekendMapper; +import lombok.extern.java.Log; +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +@Log +@Profile(SpringProfile.STEWARDING) +@Service +public class StewardingDiscordNotificationService { + private static final int COLOR_BLUE = 0x3498DB; + private static final int COLOR_RED = 0xE74C3C; + private static final int COLOR_GREEN = 0x2ECC71; + private static final int COLOR_YELLOW = 0xF1C40F; + private static final int COLOR_ORANGE = 0xE67E22; + + private final RaceWeekendMapper weekendMapper; + private final RestClient restClient; + + public StewardingDiscordNotificationService(RaceWeekendMapper weekendMapper) { + this.weekendMapper = weekendMapper; + this.restClient = RestClient.create(); + } + + @Async + public void sendIncidentNotification(Integer raceWeekendId, Incident incident) { + String webhookUrl = getWebhookUrl(raceWeekendId); + if (webhookUrl == null) { + return; + } + + Map embed = Map.of( + "title", "🚩 New Incident Reported", + "description", incident.getTitle() != null ? incident.getTitle() : "No title", + "color", COLOR_BLUE, + "fields", List.of( + Map.of("name", "Lap", "value", incident.getLap() != null ? String.valueOf(incident.getLap()) : "N/A", "inline", true), + Map.of("name", "Status", "value", incident.getStatus() != null ? incident.getStatus().getDescription() : "Reported", "inline", true) + ), + "timestamp", Instant.now().toString() + ); + + sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); + } + + @Async + public void sendDecisionNotification(Integer raceWeekendId, StewardDecision decision, Incident incident, String penaltyName) { + String webhookUrl = getWebhookUrl(raceWeekendId); + if (webhookUrl == null) { + return; + } + + boolean isNoAction = Boolean.TRUE.equals(decision.getIsNoAction()); + int color = isNoAction ? COLOR_GREEN : COLOR_RED; + String title = isNoAction ? "✅ No Further Action" : "⚖️ Steward Decision"; + + Map embed = Map.of( + "title", title, + "description", incident != null && incident.getTitle() != null ? incident.getTitle() : "Manual decision", + "color", color, + "fields", List.of( + Map.of("name", "Penalty", "value", penaltyName != null ? penaltyName : "No action", "inline", true), + Map.of("name", "Reasoning", "value", decision.getReasoning() != null ? decision.getReasoning() : "N/A", "inline", false) + ), + "timestamp", Instant.now().toString() + ); + + sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); + } + + @Async + public void sendAppealNotification(Integer raceWeekendId, Appeal appeal) { + String webhookUrl = getWebhookUrl(raceWeekendId); + if (webhookUrl == null) { + return; + } + + Map embed = Map.of( + "title", "📋 Appeal Filed", + "description", appeal.getReason() != null ? appeal.getReason() : "No reason provided", + "color", COLOR_YELLOW, + "fields", List.of( + Map.of("name", "Decision ID", "value", String.valueOf(appeal.getDecisionId()), "inline", true), + Map.of("name", "Status", "value", appeal.getStatus() != null ? appeal.getStatus().getDescription() : "Pending", "inline", true) + ), + "timestamp", Instant.now().toString() + ); + + sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); + } + + @Async + public void sendAppealReviewedNotification(Integer raceWeekendId, Appeal appeal) { + String webhookUrl = getWebhookUrl(raceWeekendId); + if (webhookUrl == null) { + return; + } + + int color = appeal.getStatus() == AppealStatus.ACCEPTED ? COLOR_GREEN : COLOR_RED; + + Map embed = Map.of( + "title", "📋 Appeal Reviewed", + "description", appeal.getResponse() != null ? appeal.getResponse() : "No response provided", + "color", color, + "fields", List.of( + Map.of("name", "Decision ID", "value", String.valueOf(appeal.getDecisionId()), "inline", true), + Map.of("name", "Result", "value", appeal.getStatus() != null ? appeal.getStatus().getDescription() : "N/A", "inline", true) + ), + "timestamp", Instant.now().toString() + ); + + sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); + } + + @Async + public void sendDecisionRevisedNotification(Integer raceWeekendId, StewardDecision oldDecision, StewardDecision newDecision) { + String webhookUrl = getWebhookUrl(raceWeekendId); + if (webhookUrl == null) { + return; + } + + Map embed = Map.of( + "title", "🔄 Decision Revised", + "description", "A previous steward decision has been revised.", + "color", COLOR_ORANGE, + "fields", List.of( + Map.of("name", "Previous Decision ID", "value", String.valueOf(oldDecision.getId()), "inline", true), + Map.of("name", "New Decision ID", "value", String.valueOf(newDecision.getId()), "inline", true), + Map.of("name", "New Reasoning", "value", newDecision.getReasoning() != null ? newDecision.getReasoning() : "N/A", "inline", false) + ), + "timestamp", Instant.now().toString() + ); + + sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); + } + + private String getWebhookUrl(Integer raceWeekendId) { + RaceWeekend weekend = weekendMapper.findById(raceWeekendId); + if (weekend == null || weekend.getDiscordWebhookUrl() == null || weekend.getDiscordWebhookUrl().isBlank()) { + return null; + } + return weekend.getDiscordWebhookUrl(); + } + + private void sendWebhook(String webhookUrl, Map payload) { + try { + restClient.post() + .uri(webhookUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(payload) + .retrieve() + .toBodilessEntity(); + } catch (Exception e) { + log.log(Level.WARNING, "Failed to send Discord webhook notification", e); + } + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java new file mode 100644 index 00000000..cdee93c7 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java @@ -0,0 +1,107 @@ +package de.sustineo.simdesk.services.stewarding; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylist; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistDriver; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistEntry; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistDriverMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistEntryMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class StewardingEntrylistService { + private final StewardingEntrylistMapper entrylistMapper; + private final StewardingEntrylistEntryMapper entryMapper; + private final StewardingEntrylistDriverMapper driverMapper; + private final ObjectMapper objectMapper; + + public StewardingEntrylist getEntrylistByWeekendId(Integer weekendId) { + List entrylists = entrylistMapper.findByRaceWeekendId(weekendId); + return entrylists.isEmpty() ? null : entrylists.getFirst(); + } + + public List getEntriesByEntrylistId(Integer entrylistId) { + return entryMapper.findByEntrylistId(entrylistId); + } + + public List getDriversByEntryId(Integer entryId) { + return driverMapper.findByEntryId(entryId); + } + + @Transactional + public void uploadEntrylist(Integer weekendId, String jsonContent) { + deleteEntrylist(weekendId); + + JsonNode root; + try { + root = objectMapper.readTree(jsonContent); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid JSON content", e); + } + + StewardingEntrylist entrylist = new StewardingEntrylist(); + entrylist.setRaceWeekendId(weekendId); + entrylist.setRawJson(jsonContent); + entrylistMapper.insert(entrylist); + + JsonNode entries = root.get("entries"); + if (entries == null || !entries.isArray()) { + return; + } + + for (JsonNode entryNode : entries) { + int raceNumber = entryNode.path("raceNumber").asInt(); + int forcedCarModel = entryNode.path("forcedCarModel").asInt(); + + JsonNode driversNode = entryNode.get("drivers"); + String displayName = "#" + raceNumber; + if (driversNode != null && driversNode.isArray() && !driversNode.isEmpty()) { + String shortName = driversNode.get(0).path("shortName").asText(""); + displayName = "#" + raceNumber + " \u2014 " + shortName; + } + + StewardingEntrylistEntry entry = new StewardingEntrylistEntry(); + entry.setEntrylistId(entrylist.getId()); + entry.setRaceNumber(raceNumber); + entry.setCarModelId(forcedCarModel); + entry.setDisplayName(displayName); + entryMapper.insert(entry); + + if (driversNode != null && driversNode.isArray()) { + for (JsonNode driverNode : driversNode) { + StewardingEntrylistDriver driver = new StewardingEntrylistDriver(); + driver.setEntryId(entry.getId()); + driver.setFirstName(driverNode.path("firstName").asText("")); + driver.setLastName(driverNode.path("lastName").asText("")); + driver.setShortName(driverNode.path("shortName").asText("")); + driver.setSteamId(driverNode.path("playerID").asText("")); + driver.setCategory(driverNode.path("driverCategory").asInt(0)); + driverMapper.insert(driver); + } + } + } + } + + @Transactional + public void deleteEntrylist(Integer weekendId) { + List existing = entrylistMapper.findByRaceWeekendId(weekendId); + for (StewardingEntrylist entrylist : existing) { + List entries = entryMapper.findByEntrylistId(entrylist.getId()); + for (StewardingEntrylistEntry entry : entries) { + driverMapper.deleteByEntryId(entry.getId()); + } + entryMapper.deleteByEntrylistId(entrylist.getId()); + } + entrylistMapper.deleteByRaceWeekendId(weekendId); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java new file mode 100644 index 00000000..5313138b --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java @@ -0,0 +1,50 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.Incident; +import de.sustineo.simdesk.entities.stewarding.IncidentStatus; +import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentInvolvedEntryMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class StewardingIncidentService { + private final StewardingIncidentMapper incidentMapper; + private final StewardingIncidentInvolvedEntryMapper involvedEntryMapper; + + public List getIncidentsBySessionId(Integer sessionId) { + return incidentMapper.findBySessionId(sessionId); + } + + public Incident getIncidentById(Integer id) { + return incidentMapper.findById(id); + } + + public List getIncidentsBySessionIdAndStatus(Integer sessionId, IncidentStatus status) { + return incidentMapper.findBySessionIdAndStatus(sessionId, status.name()); + } + + @Transactional + public void createIncident(Incident incident, List involvedEntryIds) { + incidentMapper.insert(incident); + for (Integer entryId : involvedEntryIds) { + involvedEntryMapper.insert(incident.getId(), entryId); + } + } + + @Transactional + public void updateIncidentStatus(Integer id, IncidentStatus status) { + incidentMapper.updateStatus(id, status.name()); + } + + public List getInvolvedEntryIds(Integer incidentId) { + return involvedEntryMapper.findEntryIdsByIncidentId(incidentId); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java new file mode 100644 index 00000000..84b55c2e --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java @@ -0,0 +1,41 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class StewardingTrackService { + private final StewardingTrackMapper trackMapper; + + public List getAllTracks() { + return trackMapper.findAll(); + } + + public StewardingTrack getTrackById(Integer id) { + return trackMapper.findById(id); + } + + @Transactional + public void createTrack(StewardingTrack track) { + trackMapper.insert(track); + } + + @Transactional + public void updateTrack(StewardingTrack track) { + trackMapper.update(track); + } + + @Transactional + public void deleteTrack(Integer id) { + trackMapper.delete(id); + } +} From 8c1bace8823fe7dfcc1bef4fd1ebc436a1a815ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:29:32 +0000 Subject: [PATCH 06/24] Add Vaadin UI views for SimRacing Stewarding suite Create 10 view files in the stewarding package: - RaceWeekendListView: Grid of race weekends with admin create button - RaceWeekendFormView: Form to create new race weekends - RaceWeekendDetailView: Weekend details with Sessions/Incidents/Entrylist tabs - RaceWeekendSessionDetailView: Session details with incident grid - IncidentDetailView: Incident details with decision/appeal management - StewardingTrackListView: Grid of stewarding tracks - StewardingTrackFormView: Form to create new tracks - PenaltyCatalogListView: Grid of penalty catalogs - PenaltyCatalogDetailView: Catalog details with penalty definitions - ReasoningTemplateListView: Grid of reasoning templates with dialog form All views follow existing codebase patterns: extend BaseView, use @Profile(SpringProfile.STEWARDING), MainLayout routing, and initialize UI in beforeEnter(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../views/stewarding/IncidentDetailView.java | 284 ++++++++++++++++++ .../stewarding/PenaltyCatalogDetailView.java | 215 +++++++++++++ .../stewarding/PenaltyCatalogListView.java | 68 +++++ .../stewarding/RaceWeekendDetailView.java | 210 +++++++++++++ .../views/stewarding/RaceWeekendFormView.java | 125 ++++++++ .../views/stewarding/RaceWeekendListView.java | 81 +++++ .../RaceWeekendSessionDetailView.java | 124 ++++++++ .../stewarding/ReasoningTemplateListView.java | 123 ++++++++ .../stewarding/StewardingTrackFormView.java | 90 ++++++ .../stewarding/StewardingTrackListView.java | 62 ++++ 10 files changed, 1382 insertions(+) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java new file mode 100644 index 00000000..de25d2ea --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -0,0 +1,284 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.*; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.*; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/weekends/:weekendId/incidents/:incidentId", layout = MainLayout.class) +@AnonymousAllowed +public class IncidentDetailView extends BaseView { + private final StewardingIncidentService incidentService; + private final StewardDecisionService decisionService; + private final StewardingAppealService appealService; + private final PenaltyCatalogService catalogService; + private final ReasoningTemplateService templateService; + private final RaceWeekendService raceWeekendService; + private final StewardingEntrylistService entrylistService; + private final SecurityService securityService; + + public IncidentDetailView(StewardingIncidentService incidentService, StewardDecisionService decisionService, + StewardingAppealService appealService, PenaltyCatalogService catalogService, + ReasoningTemplateService templateService, RaceWeekendService raceWeekendService, + StewardingEntrylistService entrylistService, SecurityService securityService) { + this.incidentService = incidentService; + this.decisionService = decisionService; + this.appealService = appealService; + this.catalogService = catalogService; + this.templateService = templateService; + this.raceWeekendService = raceWeekendService; + this.entrylistService = entrylistService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Incident Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); + String incidentIdParam = event.getRouteParameters().get("incidentId").orElse(null); + if (weekendIdParam == null || incidentIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + Integer weekendId; + Integer incidentId; + try { + weekendId = Integer.valueOf(weekendIdParam); + incidentId = Integer.valueOf(incidentIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); + Incident incident = incidentService.getIncidentById(incidentId); + if (weekend == null || incident == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + add(createViewHeader(incident.getTitle())); + + // Incident details + VerticalLayout detailsLayout = new VerticalLayout(); + detailsLayout.setPadding(true); + detailsLayout.setSpacing(false); + + if (incident.getDescription() != null && !incident.getDescription().isEmpty()) { + detailsLayout.add(new Paragraph(incident.getDescription())); + } + if (incident.getLap() != null) { + detailsLayout.add(new Paragraph("Lap: " + incident.getLap())); + } + if (incident.getTimestampInSession() != null) { + detailsLayout.add(new Paragraph("Time in Session: " + incident.getTimestampInSession())); + } + if (incident.getInvolvedCarsText() != null) { + detailsLayout.add(new Paragraph("Involved Cars: " + incident.getInvolvedCarsText())); + } + if (incident.getVideoUrl() != null && !incident.getVideoUrl().isEmpty()) { + Anchor videoLink = new Anchor(incident.getVideoUrl(), "Video Evidence"); + videoLink.setTarget("_blank"); + detailsLayout.add(videoLink); + } + detailsLayout.add(new Paragraph("Status: " + (incident.getStatus() != null ? incident.getStatus().getDescription() : "-"))); + add(detailsLayout); + + // Steward Decision section (ADMIN only) + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + add(createDecisionSection(incident, weekend)); + } + + // Decision History + add(createDecisionHistorySection(incidentId)); + + // Appeals section + add(createAppealsSection(incidentId)); + } + + private VerticalLayout createDecisionSection(Incident incident, RaceWeekend weekend) { + VerticalLayout layout = new VerticalLayout(); + layout.setPadding(true); + layout.add(new H3("Steward Decision")); + + StewardDecision activeDecision = decisionService.getActiveDecisionByIncidentId(incident.getId()); + if (activeDecision != null) { + layout.add(new Paragraph("Penalty: " + (activeDecision.getCustomPenalty() != null ? activeDecision.getCustomPenalty() : "-"))); + layout.add(new Paragraph("Reasoning: " + (activeDecision.getReasoning() != null ? activeDecision.getReasoning() : "-"))); + layout.add(new Paragraph("No Further Action: " + (Boolean.TRUE.equals(activeDecision.getIsNoAction()) ? "Yes" : "No"))); + + Button reviseButton = new Button("Revise Decision"); + reviseButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + reviseButton.addClickListener(e -> { + layout.removeAll(); + layout.add(new H3("Revise Decision")); + layout.add(createDecisionForm(incident, weekend, activeDecision.getId())); + }); + layout.add(reviseButton); + } else { + layout.add(createDecisionForm(incident, weekend, null)); + } + + return layout; + } + + private FormLayout createDecisionForm(Incident incident, RaceWeekend weekend, Integer existingDecisionId) { + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); + + ComboBox penaltyCombo = new ComboBox<>("Penalty"); + if (weekend.getPenaltyCatalogId() != null && session != null && session.getSessionType() != null) { + List definitions = catalogService.getDefinitionsForSessionType( + weekend.getPenaltyCatalogId(), session.getSessionType().name()); + penaltyCombo.setItems(definitions); + penaltyCombo.setItemLabelGenerator(d -> d.getCode() + " - " + d.getName()); + } + + TextField customPenaltyField = new TextField("Custom Penalty"); + customPenaltyField.setWidthFull(); + + TextArea reasoningField = new TextArea("Reasoning"); + reasoningField.setWidthFull(); + + ComboBox templateCombo = new ComboBox<>("Reasoning Template"); + templateCombo.setItems(templateService.getAllTemplates()); + templateCombo.setItemLabelGenerator(ReasoningTemplate::getName); + templateCombo.addValueChangeListener(e -> { + if (e.getValue() != null) { + reasoningField.setValue(e.getValue().getTemplateText()); + } + }); + + TextField penalizedCarField = new TextField("Penalized Car"); + penalizedCarField.setWidthFull(); + + Checkbox noActionCheckbox = new Checkbox("No Further Action"); + + form.add(penaltyCombo, customPenaltyField, templateCombo, reasoningField, penalizedCarField, noActionCheckbox); + + Button saveButton = new Button("Save Decision", e -> { + StewardDecision decision = StewardDecision.builder() + .incidentId(incident.getId()) + .sessionId(incident.getSessionId()) + .penaltyDefinitionId(penaltyCombo.getValue() != null ? penaltyCombo.getValue().getId() : null) + .customPenalty(customPenaltyField.getValue()) + .reasoning(reasoningField.getValue()) + .reasoningTemplateId(templateCombo.getValue() != null ? templateCombo.getValue().getId() : null) + .isNoAction(noActionCheckbox.getValue()) + .penalizedCarText(penalizedCarField.getValue()) + .isActive(true) + .build(); + + if (existingDecisionId != null) { + decisionService.reviseDecision(existingDecisionId, decision); + } else { + decisionService.makeDecision(decision); + } + + Notification.show("Decision saved", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + form.add(saveButton); + + return form; + } + + private VerticalLayout createDecisionHistorySection(Integer incidentId) { + VerticalLayout layout = new VerticalLayout(); + layout.setPadding(true); + layout.add(new H3("Decision History")); + + List history = decisionService.getDecisionHistory(incidentId); + if (history.isEmpty()) { + layout.add(new Paragraph("No decisions yet.")); + return layout; + } + + for (StewardDecision decision : history) { + HorizontalLayout row = new HorizontalLayout(); + row.setAlignItems(Alignment.CENTER); + + Span status = new Span(Boolean.TRUE.equals(decision.getIsActive()) ? "ACTIVE" : "SUPERSEDED"); + Span penalty = new Span(decision.getCustomPenalty() != null ? decision.getCustomPenalty() : "-"); + Span reasoning = new Span(decision.getReasoning() != null ? decision.getReasoning() : "-"); + Span decidedAt = new Span(decision.getDecidedAt() != null ? decision.getDecidedAt().toString() : "-"); + + row.add(status, penalty, reasoning, decidedAt); + layout.add(row); + } + + return layout; + } + + private VerticalLayout createAppealsSection(Integer incidentId) { + VerticalLayout layout = new VerticalLayout(); + layout.setPadding(true); + layout.add(new H3("Appeals")); + + StewardDecision activeDecision = decisionService.getActiveDecisionByIncidentId(incidentId); + if (activeDecision == null) { + layout.add(new Paragraph("No active decision to appeal.")); + return layout; + } + + List appeals = appealService.getAppealsByDecisionId(activeDecision.getId()); + if (appeals.isEmpty()) { + layout.add(new Paragraph("No appeals filed.")); + } else { + Grid grid = new Grid<>(Appeal.class, false); + grid.addColumn(Appeal::getReason).setHeader("Reason").setAutoWidth(true); + grid.addColumn(appeal -> appeal.getStatus() != null ? appeal.getStatus().getDescription() : "-") + .setHeader("Status").setAutoWidth(true); + grid.addColumn(appeal -> appeal.getResponse() != null ? appeal.getResponse() : "-") + .setHeader("Response").setAutoWidth(true); + grid.addColumn(Appeal::getFiledAt).setHeader("Filed At").setAutoWidth(true); + grid.setItems(appeals); + layout.add(grid); + } + + return layout; + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java new file mode 100644 index 00000000..55736ec6 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java @@ -0,0 +1,215 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; +import de.sustineo.simdesk.entities.stewarding.PenaltySessionType; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/catalogs/:catalogId", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class PenaltyCatalogDetailView extends BaseView { + private final PenaltyCatalogService catalogService; + + public PenaltyCatalogDetailView(PenaltyCatalogService catalogService) { + this.catalogService = catalogService; + } + + @Override + public String getPageTitle() { + return "Penalty Catalog Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String catalogIdParam = event.getRouteParameters().get("catalogId").orElse(null); + if (catalogIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); + return; + } + + // Handle "new" catalog creation + if ("new".equals(catalogIdParam)) { + add(createViewHeader("New Penalty Catalog")); + add(createNewCatalogForm()); + return; + } + + Integer catalogId; + try { + catalogId = Integer.valueOf(catalogIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); + return; + } + + PenaltyCatalog catalog = catalogService.getCatalogById(catalogId); + if (catalog == null) { + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); + return; + } + + add(createViewHeader(catalog.getName())); + + VerticalLayout infoLayout = new VerticalLayout(); + infoLayout.setPadding(true); + infoLayout.setSpacing(false); + if (catalog.getDescription() != null && !catalog.getDescription().isEmpty()) { + infoLayout.add(new Paragraph(catalog.getDescription())); + } + add(infoLayout); + + HorizontalLayout actionLayout = new HorizontalLayout(); + Button addPenaltyButton = new Button("Add Penalty", e -> openPenaltyDialog(catalogId)); + addPenaltyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + actionLayout.add(addPenaltyButton); + add(actionLayout); + + List definitions = catalogService.getDefinitionsByCatalogId(catalogId); + Grid grid = new Grid<>(PenaltyDefinition.class, false); + grid.addColumn(PenaltyDefinition::getCode).setHeader("Code").setAutoWidth(true); + grid.addColumn(PenaltyDefinition::getName).setHeader("Name").setAutoWidth(true); + grid.addColumn(PenaltyDefinition::getCategory).setHeader("Category").setAutoWidth(true); + grid.addColumn(def -> def.getSessionType() != null ? def.getSessionType().getDescription() : "-") + .setHeader("Session Type").setAutoWidth(true); + grid.addColumn(PenaltyDefinition::getDefaultPenalty).setHeader("Default Penalty").setAutoWidth(true); + grid.addColumn(PenaltyDefinition::getSeverity).setHeader("Severity").setAutoWidth(true); + grid.setItems(definitions); + grid.setSizeFull(); + + addAndExpand(grid); + } + + private FormLayout createNewCatalogForm() { + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField nameField = new TextField("Name"); + nameField.setRequired(true); + nameField.setWidthFull(); + + TextField descriptionField = new TextField("Description"); + descriptionField.setWidthFull(); + + form.add(nameField, descriptionField); + + Button saveButton = new Button("Save", e -> { + if (nameField.isEmpty()) { + Notification.show("Name is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + PenaltyCatalog catalog = PenaltyCatalog.builder() + .name(nameField.getValue()) + .description(descriptionField.getValue()) + .build(); + catalogService.createCatalog(catalog); + Notification.show("Catalog created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class))); + + HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); + form.add(buttonLayout, 2); + + return form; + } + + private void openPenaltyDialog(Integer catalogId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Add Penalty Definition"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); + + TextField codeField = new TextField("Code"); + codeField.setRequired(true); + + TextField nameField = new TextField("Name"); + nameField.setRequired(true); + + TextField categoryField = new TextField("Category"); + + ComboBox sessionTypeCombo = new ComboBox<>("Session Type"); + sessionTypeCombo.setItems(PenaltySessionType.values()); + sessionTypeCombo.setItemLabelGenerator(PenaltySessionType::getDescription); + + TextField defaultPenaltyField = new TextField("Default Penalty"); + + IntegerField severityField = new IntegerField("Severity"); + severityField.setMin(0); + + IntegerField sortOrderField = new IntegerField("Sort Order"); + sortOrderField.setMin(0); + + form.add(codeField, nameField, categoryField, sessionTypeCombo, defaultPenaltyField, severityField, sortOrderField); + + Button saveButton = new Button("Save", e -> { + if (codeField.isEmpty() || nameField.isEmpty()) { + Notification.show("Code and Name are required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + PenaltyDefinition definition = PenaltyDefinition.builder() + .catalogId(catalogId) + .code(codeField.getValue()) + .name(nameField.getValue()) + .category(categoryField.getValue()) + .sessionType(sessionTypeCombo.getValue()) + .defaultPenalty(defaultPenaltyField.getValue()) + .severity(severityField.getValue()) + .sortOrder(sortOrderField.getValue()) + .build(); + + catalogService.createDefinition(definition); + dialog.close(); + Notification.show("Penalty definition added", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java new file mode 100644 index 00000000..db8c2d9b --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java @@ -0,0 +1,68 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParameters; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/catalogs", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class PenaltyCatalogListView extends BaseView { + private final PenaltyCatalogService catalogService; + + public PenaltyCatalogListView(PenaltyCatalogService catalogService) { + this.catalogService = catalogService; + } + + @Override + public String getPageTitle() { + return "Penalty Catalogs"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.setWidthFull(); + headerLayout.setAlignItems(Alignment.CENTER); + headerLayout.add(createViewHeader()); + + Button newButton = new Button("New Catalog", e -> + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogDetailView.class, + new RouteParameters("catalogId", "new")))); + newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + headerLayout.add(newButton); + + add(headerLayout); + + List catalogs = catalogService.getAllCatalogs(); + Grid grid = new Grid<>(PenaltyCatalog.class, false); + grid.addColumn(PenaltyCatalog::getName).setHeader("Name").setAutoWidth(true); + grid.addColumn(PenaltyCatalog::getDescription).setHeader("Description").setAutoWidth(true); + grid.setItems(catalogs); + grid.setSizeFull(); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogDetailView.class, + new RouteParameters("catalogId", String.valueOf(e.getItem().getId())))) + ); + + addAndExpand(grid); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java new file mode 100644 index 00000000..827083cf --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java @@ -0,0 +1,210 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.tabs.TabSheet; +import com.vaadin.flow.component.upload.Upload; +import com.vaadin.flow.server.streams.UploadHandler; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParam; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.*; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.RaceWeekendService; +import de.sustineo.simdesk.services.stewarding.StewardingEntrylistService; +import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/weekends/:weekendId", layout = MainLayout.class) +@AnonymousAllowed +public class RaceWeekendDetailView extends BaseView { + private final RaceWeekendService raceWeekendService; + private final StewardingIncidentService incidentService; + private final StewardingEntrylistService entrylistService; + private final SecurityService securityService; + + public RaceWeekendDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, + StewardingEntrylistService entrylistService, SecurityService securityService) { + this.raceWeekendService = raceWeekendService; + this.incidentService = incidentService; + this.entrylistService = entrylistService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Race Weekend Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); + if (weekendIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + Integer weekendId; + try { + weekendId = Integer.valueOf(weekendIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); + if (weekend == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + add(createViewHeader(weekend.getTitle())); + + VerticalLayout infoLayout = new VerticalLayout(); + infoLayout.setPadding(true); + infoLayout.setSpacing(false); + if (weekend.getDescription() != null && !weekend.getDescription().isEmpty()) { + infoLayout.add(new Paragraph(weekend.getDescription())); + } + if (weekend.getTrack() != null) { + infoLayout.add(new Paragraph("Track: " + weekend.getTrack().getName())); + } + if (weekend.getStartDate() != null && weekend.getEndDate() != null) { + infoLayout.add(new Paragraph("Date: " + weekend.getStartDate() + " — " + weekend.getEndDate())); + } + add(infoLayout); + + TabSheet tabSheet = new TabSheet(); + tabSheet.setSizeFull(); + + tabSheet.add("Sessions", createSessionsTab(weekendId)); + tabSheet.add("Incidents", createIncidentsTab(weekendId)); + tabSheet.add("Entrylist", createEntrylistTab(weekendId)); + + addAndExpand(tabSheet); + } + + private VerticalLayout createSessionsTab(Integer weekendId) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + Button addSessionButton = new Button("Add Session"); + addSessionButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + layout.add(addSessionButton); + } + + List sessions = raceWeekendService.getSessionsByWeekendId(weekendId); + Grid grid = new Grid<>(RaceWeekendSession.class, false); + grid.addColumn(session -> session.getSessionType() != null ? session.getSessionType().getDescription() : "-") + .setHeader("Type").setAutoWidth(true); + grid.addColumn(RaceWeekendSession::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(RaceWeekendSession::getStartTime).setHeader("Start Time").setAutoWidth(true); + grid.addColumn(RaceWeekendSession::getEndTime).setHeader("End Time").setAutoWidth(true); + grid.setItems(sessions); + grid.setSizeFull(); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendSessionDetailView.class, + new RouteParameters( + new RouteParam("weekendId", String.valueOf(weekendId)), + new RouteParam("sessionId", String.valueOf(e.getItem().getId())) + ))) + ); + + layout.addAndExpand(grid); + return layout; + } + + private VerticalLayout createIncidentsTab(Integer weekendId) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + + List sessions = raceWeekendService.getSessionsByWeekendId(weekendId); + List allIncidents = new ArrayList<>(); + for (RaceWeekendSession session : sessions) { + allIncidents.addAll(incidentService.getIncidentsBySessionId(session.getId())); + } + + Grid grid = new Grid<>(Incident.class, false); + grid.addColumn(incident -> { + RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); + return session != null ? session.getTitle() : "-"; + }).setHeader("Session").setAutoWidth(true); + grid.addColumn(Incident::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") + .setHeader("Status").setAutoWidth(true); + grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true); + grid.setItems(allIncidents); + grid.setSizeFull(); + + layout.addAndExpand(grid); + return layout; + } + + private VerticalLayout createEntrylistTab(Integer weekendId) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + Upload upload = new Upload(); + upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { + String json = new String(data, StandardCharsets.UTF_8); + try { + entrylistService.uploadEntrylist(weekendId, json); + getUI().ifPresent(ui -> ui.access(() -> { + Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + ui.getPage().reload(); + })); + } catch (IllegalArgumentException ex) { + getUI().ifPresent(ui -> ui.access(() -> + Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR) + )); + } + })); + upload.setAcceptedFileTypes("application/json", ".json"); + upload.setMaxFiles(1); + layout.add(upload); + } + + StewardingEntrylist entrylist = entrylistService.getEntrylistByWeekendId(weekendId); + if (entrylist != null) { + List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); + Grid grid = new Grid<>(StewardingEntrylistEntry.class, false); + grid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true); + grid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setAutoWidth(true); + grid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setAutoWidth(true); + grid.setItems(entries); + grid.setSizeFull(); + layout.addAndExpand(grid); + } else { + layout.add(new H3("No entrylist uploaded yet")); + } + + return layout; + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java new file mode 100644 index 00000000..8b021a63 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java @@ -0,0 +1,125 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; +import de.sustineo.simdesk.services.stewarding.RaceWeekendService; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/weekends/new", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class RaceWeekendFormView extends BaseView { + private final RaceWeekendService raceWeekendService; + private final StewardingTrackService trackService; + private final PenaltyCatalogService catalogService; + + public RaceWeekendFormView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, PenaltyCatalogService catalogService) { + this.raceWeekendService = raceWeekendService; + this.trackService = trackService; + this.catalogService = catalogService; + } + + @Override + public String getPageTitle() { + return "New Race Weekend"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + add(createViewHeader()); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + + ComboBox trackCombo = new ComboBox<>("Track"); + trackCombo.setItems(trackService.getAllTracks()); + trackCombo.setItemLabelGenerator(StewardingTrack::getName); + trackCombo.setWidthFull(); + + ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); + catalogCombo.setItems(catalogService.getAllCatalogs()); + catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); + catalogCombo.setWidthFull(); + + TextField webhookField = new TextField("Discord Webhook URL"); + webhookField.setWidthFull(); + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + + FormLayout formLayout = new FormLayout(); + formLayout.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + formLayout.add(titleField, 2); + formLayout.add(descriptionField, 2); + formLayout.add(trackCombo); + formLayout.add(catalogCombo); + formLayout.add(webhookField, 2); + formLayout.add(startDatePicker); + formLayout.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + RaceWeekend weekend = RaceWeekend.builder() + .title(titleField.getValue()) + .description(descriptionField.getValue()) + .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) + .penaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null) + .discordWebhookUrl(webhookField.getValue()) + .startDate(startDatePicker.getValue()) + .endDate(endDatePicker.getValue()) + .build(); + + raceWeekendService.createWeekend(weekend); + Notification.show("Race weekend created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class))); + + HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); + + add(formLayout, buttonLayout); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java new file mode 100644 index 00000000..910f5f21 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java @@ -0,0 +1,81 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.RaceWeekendService; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/weekends", layout = MainLayout.class) +@AnonymousAllowed +public class RaceWeekendListView extends BaseView { + private final RaceWeekendService raceWeekendService; + private final StewardingTrackService trackService; + private final SecurityService securityService; + + public RaceWeekendListView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, SecurityService securityService) { + this.raceWeekendService = raceWeekendService; + this.trackService = trackService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Race Weekends"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.setWidthFull(); + headerLayout.setAlignItems(Alignment.CENTER); + headerLayout.add(createViewHeader()); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + Button newButton = new Button("New Weekend", e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendFormView.class))); + newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + headerLayout.add(newButton); + } + + add(headerLayout); + + List weekends = raceWeekendService.getAllWeekends(); + Grid grid = new Grid<>(RaceWeekend.class, false); + grid.addColumn(RaceWeekend::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(weekend -> { + var track = trackService.getTrackById(weekend.getTrackId()); + return track != null ? track.getName() : "-"; + }).setHeader("Track").setAutoWidth(true); + grid.addColumn(RaceWeekend::getStartDate).setHeader("Start Date").setAutoWidth(true); + grid.addColumn(RaceWeekend::getEndDate).setHeader("End Date").setAutoWidth(true); + grid.setItems(weekends); + grid.setSizeFull(); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, + new RouteParameters("weekendId", String.valueOf(e.getItem().getId())))) + ); + + addAndExpand(grid); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java new file mode 100644 index 00000000..048a0320 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java @@ -0,0 +1,124 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParam; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.Incident; +import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.RaceWeekendService; +import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/weekends/:weekendId/sessions/:sessionId", layout = MainLayout.class) +@AnonymousAllowed +public class RaceWeekendSessionDetailView extends BaseView { + private final RaceWeekendService raceWeekendService; + private final StewardingIncidentService incidentService; + private final SecurityService securityService; + + public RaceWeekendSessionDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, + SecurityService securityService) { + this.raceWeekendService = raceWeekendService; + this.incidentService = incidentService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Session Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); + String sessionIdParam = event.getRouteParameters().get("sessionId").orElse(null); + if (weekendIdParam == null || sessionIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + Integer weekendId; + Integer sessionId; + try { + weekendId = Integer.valueOf(weekendIdParam); + sessionId = Integer.valueOf(sessionIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); + RaceWeekendSession session = raceWeekendService.getSessionById(sessionId); + if (weekend == null || session == null) { + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + return; + } + + add(createViewHeader(session.getTitle())); + + VerticalLayout infoLayout = new VerticalLayout(); + infoLayout.setPadding(true); + infoLayout.setSpacing(false); + if (session.getSessionType() != null) { + infoLayout.add(new Paragraph("Type: " + session.getSessionType().getDescription())); + } + if (session.getStartTime() != null) { + infoLayout.add(new Paragraph("Start: " + session.getStartTime())); + } + if (session.getEndTime() != null) { + infoLayout.add(new Paragraph("End: " + session.getEndTime())); + } + add(infoLayout); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + HorizontalLayout actionLayout = new HorizontalLayout(); + Button reportIncidentButton = new Button("Report Incident"); + reportIncidentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + Button quickDecisionButton = new Button("Quick Decision"); + quickDecisionButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); + actionLayout.add(reportIncidentButton, quickDecisionButton); + add(actionLayout); + } + + List incidents = incidentService.getIncidentsBySessionId(sessionId); + Grid grid = new Grid<>(Incident.class, false); + grid.addColumn(Incident::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") + .setHeader("Status").setAutoWidth(true); + grid.addColumn(Incident::getInvolvedCarsText).setHeader("Involved Cars").setAutoWidth(true); + grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true); + grid.setItems(incidents); + grid.setSizeFull(); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(IncidentDetailView.class, + new RouteParameters( + new RouteParam("weekendId", String.valueOf(weekendId)), + new RouteParam("incidentId", String.valueOf(e.getItem().getId())) + ))) + ); + + addAndExpand(grid); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java new file mode 100644 index 00000000..eb4fc484 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java @@ -0,0 +1,123 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.ReasoningTemplateService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/templates", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class ReasoningTemplateListView extends BaseView { + private final ReasoningTemplateService templateService; + + public ReasoningTemplateListView(ReasoningTemplateService templateService) { + this.templateService = templateService; + } + + @Override + public String getPageTitle() { + return "Reasoning Templates"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.setWidthFull(); + headerLayout.setAlignItems(Alignment.CENTER); + headerLayout.add(createViewHeader()); + + Button newButton = new Button("New Template", e -> openTemplateDialog()); + newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + headerLayout.add(newButton); + + add(headerLayout); + + List templates = templateService.getAllTemplates(); + Grid grid = new Grid<>(ReasoningTemplate.class, false); + grid.addColumn(ReasoningTemplate::getName).setHeader("Name").setAutoWidth(true); + grid.addColumn(ReasoningTemplate::getCategory).setHeader("Category").setAutoWidth(true); + grid.addColumn(template -> { + String text = template.getTemplateText(); + if (text != null && text.length() > 100) { + return text.substring(0, 100) + "..."; + } + return text != null ? text : "-"; + }).setHeader("Template Text").setAutoWidth(true); + grid.setItems(templates); + grid.setSizeFull(); + + addAndExpand(grid); + } + + private void openTemplateDialog() { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("New Reasoning Template"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); + + TextField nameField = new TextField("Name"); + nameField.setRequired(true); + nameField.setWidthFull(); + + TextField categoryField = new TextField("Category"); + categoryField.setWidthFull(); + + TextArea templateTextField = new TextArea("Template Text"); + templateTextField.setWidthFull(); + templateTextField.setMinHeight("150px"); + + form.add(nameField, categoryField, templateTextField); + + Button saveButton = new Button("Save", e -> { + if (nameField.isEmpty()) { + Notification.show("Name is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + ReasoningTemplate template = ReasoningTemplate.builder() + .name(nameField.getValue()) + .category(categoryField.getValue()) + .templateText(templateTextField.getValue()) + .build(); + + templateService.createTemplate(template); + dialog.close(); + Notification.show("Template created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java new file mode 100644 index 00000000..025b6ce7 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java @@ -0,0 +1,90 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/tracks/new", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class StewardingTrackFormView extends BaseView { + private final StewardingTrackService trackService; + + public StewardingTrackFormView(StewardingTrackService trackService) { + this.trackService = trackService; + } + + @Override + public String getPageTitle() { + return "New Track"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + add(createViewHeader()); + + TextField nameField = new TextField("Name"); + nameField.setWidthFull(); + nameField.setRequired(true); + + TextField countryField = new TextField("Country"); + countryField.setWidthFull(); + + TextField mapImageUrlField = new TextField("Map Image URL"); + mapImageUrlField.setWidthFull(); + + FormLayout formLayout = new FormLayout(); + formLayout.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + formLayout.add(nameField); + formLayout.add(countryField); + formLayout.add(mapImageUrlField, 2); + + Button saveButton = new Button("Save", e -> { + if (nameField.isEmpty()) { + Notification.show("Name is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + StewardingTrack track = StewardingTrack.builder() + .name(nameField.getValue()) + .country(countryField.getValue()) + .mapImageUrl(mapImageUrlField.getValue()) + .build(); + + trackService.createTrack(track); + Notification.show("Track created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.navigate(StewardingTrackListView.class)); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> + getUI().ifPresent(ui -> ui.navigate(StewardingTrackListView.class))); + + HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); + + add(formLayout, buttonLayout); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java new file mode 100644 index 00000000..cd52bcdd --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java @@ -0,0 +1,62 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/tracks", layout = MainLayout.class) +@RolesAllowed({"ADMIN"}) +public class StewardingTrackListView extends BaseView { + private final StewardingTrackService trackService; + + public StewardingTrackListView(StewardingTrackService trackService) { + this.trackService = trackService; + } + + @Override + public String getPageTitle() { + return "Stewarding Tracks"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.setWidthFull(); + headerLayout.setAlignItems(Alignment.CENTER); + headerLayout.add(createViewHeader()); + + Button newButton = new Button("New Track", e -> + getUI().ifPresent(ui -> ui.navigate(StewardingTrackFormView.class))); + newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + headerLayout.add(newButton); + + add(headerLayout); + + List tracks = trackService.getAllTracks(); + Grid grid = new Grid<>(StewardingTrack.class, false); + grid.addColumn(StewardingTrack::getName).setHeader("Name").setAutoWidth(true); + grid.addColumn(StewardingTrack::getCountry).setHeader("Country").setAutoWidth(true); + grid.setItems(tracks); + grid.setSizeFull(); + + addAndExpand(grid); + } +} From 2cc500a6fa9a1003b955f6c317cc5822de0f0340 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:31:18 +0000 Subject: [PATCH 07/24] Add stewarding Vaadin views and menu navigation integration Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../simdesk/entities/menu/MenuEntityCategory.java | 1 + .../de/sustineo/simdesk/services/MenuService.java | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/menu/MenuEntityCategory.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/menu/MenuEntityCategory.java index 7d9372a0..1feffb7c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/menu/MenuEntityCategory.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/menu/MenuEntityCategory.java @@ -8,6 +8,7 @@ public enum MenuEntityCategory { LIVE_TIMING("Live Timing"), ENTRYLIST("Entrylist"), BALANCE_OF_PERFORMANCE("Balance of Performance"), + STEWARDING("Stewarding"), MAP("Map"); private final String name; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java index dc44c14a..5765fccd 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java @@ -8,6 +8,7 @@ import de.sustineo.simdesk.entities.menu.MenuEntityCategory; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.views.*; +import de.sustineo.simdesk.views.stewarding.*; import lombok.Getter; import org.springframework.stereotype.Service; @@ -55,6 +56,16 @@ public List getItems() { items.add(MenuEntity.ofInternal(MenuEntityCategory.MAP, "Map", VaadinIcon.MAP_MARKER, MapView.class)); } + if (SpringProfile.isStewardingEnabled()) { + items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Race Weekends", VaadinIcon.CALENDAR, RaceWeekendListView.class)); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Tracks", VaadinIcon.ROAD, StewardingTrackListView.class)); + items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Penalty Catalogs", VaadinIcon.BOOK, PenaltyCatalogListView.class)); + items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Templates", VaadinIcon.FILE_TEXT_O, ReasoningTemplateListView.class)); + } + } + return items; } From 5a2c96db0c13a5bccd34f6fe0086322be8057a2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:34:17 +0000 Subject: [PATCH 08/24] Add test classes for SimRacing Stewarding services Add unit tests for ReasoningTemplateService, StewardingEntrylistService, and PenaltyCatalogService following existing codebase test patterns with @SpringBootTest, @ActiveProfiles, and @MockitoBean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../stewarding/PenaltyCatalogServiceTest.java | 71 +++++++++++ .../ReasoningTemplateServiceTest.java | 100 +++++++++++++++ .../StewardingEntrylistServiceTest.java | 115 ++++++++++++++++++ 3 files changed, 286 insertions(+) create mode 100644 simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java create mode 100644 simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java create mode 100644 simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java new file mode 100644 index 00000000..3c0b2e8e --- /dev/null +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java @@ -0,0 +1,71 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; +import de.sustineo.simdesk.entities.stewarding.PenaltySessionType; +import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; +import de.sustineo.simdesk.mybatis.mapper.PenaltyDefinitionMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ActiveProfiles({SpringProfile.STEWARDING}) +@SpringBootTest(classes = { + PenaltyCatalogService.class, + SpringProfile.class +}) +class PenaltyCatalogServiceTest { + + @Autowired + private PenaltyCatalogService penaltyCatalogService; + + @MockitoBean + private PenaltyCatalogMapper penaltyCatalogMapper; + + @MockitoBean + private PenaltyDefinitionMapper penaltyDefinitionMapper; + + @Test + void getDefinitionsForSessionType_shouldReturnMatchingDefinitions() { + PenaltyDefinition racePenalty = PenaltyDefinition.builder() + .id(1) + .catalogId(1) + .code("PEN-001") + .name("Causing a collision") + .sessionType(PenaltySessionType.RACE) + .defaultPenalty("5 second time penalty") + .build(); + + when(penaltyDefinitionMapper.findByCatalogIdAndSessionType(1, "RACE")) + .thenReturn(List.of(racePenalty)); + + List result = penaltyCatalogService.getDefinitionsForSessionType(1, "RACE"); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getName()).isEqualTo("Causing a collision"); + } + + @Test + void getAllCatalogs_shouldReturnAll() { + PenaltyCatalog catalog = PenaltyCatalog.builder() + .id(1) + .name("2025 Season Rules") + .description("Standard penalty rules for 2025") + .build(); + + when(penaltyCatalogMapper.findAll()).thenReturn(List.of(catalog)); + + List result = penaltyCatalogService.getAllCatalogs(); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getName()).isEqualTo("2025 Season Rules"); + } +} diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java new file mode 100644 index 00000000..95b535f9 --- /dev/null +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java @@ -0,0 +1,100 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; +import de.sustineo.simdesk.mybatis.mapper.ReasoningTemplateMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ActiveProfiles({SpringProfile.STEWARDING}) +@SpringBootTest(classes = { + ReasoningTemplateService.class, + SpringProfile.class +}) +class ReasoningTemplateServiceTest { + + @Autowired + private ReasoningTemplateService reasoningTemplateService; + + @MockitoBean + private ReasoningTemplateMapper reasoningTemplateMapper; + + @Test + void renderTemplate_shouldReplacePlaceholders() { + String template = "Car {car_a} made contact with Car {car_b} at {location}."; + Map variables = Map.of( + "car_a", "#001", + "car_b", "#042", + "location", "Turn 5" + ); + + String result = reasoningTemplateService.renderTemplate(template, variables); + + assertThat(result).isEqualTo("Car #001 made contact with Car #042 at Turn 5."); + } + + @Test + void renderTemplate_shouldKeepUnmatchedPlaceholders() { + String template = "Car {car_a} was penalized for {reason}."; + Map variables = Map.of("car_a", "#001"); + + String result = reasoningTemplateService.renderTemplate(template, variables); + + assertThat(result).isEqualTo("Car #001 was penalized for {reason}."); + } + + @Test + void renderTemplate_shouldHandleEmptyVariables() { + String template = "No placeholders here."; + Map variables = Map.of(); + + String result = reasoningTemplateService.renderTemplate(template, variables); + + assertThat(result).isEqualTo("No placeholders here."); + } + + @Test + void getAllTemplates_shouldReturnAll() { + ReasoningTemplate template = ReasoningTemplate.builder() + .id(1) + .name("Test Template") + .category("Contact") + .templateText("Template text") + .sortOrder(0) + .build(); + + when(reasoningTemplateMapper.findAll()).thenReturn(List.of(template)); + + List result = reasoningTemplateService.getAllTemplates(); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getName()).isEqualTo("Test Template"); + } + + @Test + void getTemplatesByCategory_shouldFilterByCategory() { + ReasoningTemplate template = ReasoningTemplate.builder() + .id(1) + .name("Contact Template") + .category("Contact") + .templateText("Contact template text") + .sortOrder(0) + .build(); + + when(reasoningTemplateMapper.findByCategory("Contact")).thenReturn(List.of(template)); + + List result = reasoningTemplateService.getTemplatesByCategory("Contact"); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getCategory()).isEqualTo("Contact"); + } +} diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java new file mode 100644 index 00000000..546925e0 --- /dev/null +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java @@ -0,0 +1,115 @@ +package de.sustineo.simdesk.services.stewarding; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylist; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistDriver; +import de.sustineo.simdesk.entities.stewarding.StewardingEntrylistEntry; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistDriverMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistEntryMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ActiveProfiles({SpringProfile.STEWARDING}) +@SpringBootTest(classes = { + StewardingEntrylistService.class, + ObjectMapper.class, + SpringProfile.class +}) +class StewardingEntrylistServiceTest { + + @Autowired + private StewardingEntrylistService entrylistService; + + @MockitoBean + private StewardingEntrylistMapper entrylistMapper; + + @MockitoBean + private StewardingEntrylistEntryMapper entrylistEntryMapper; + + @MockitoBean + private StewardingEntrylistDriverMapper entrylistDriverMapper; + + @Test + void uploadEntrylist_shouldParseValidAccJson() { + when(entrylistMapper.findByRaceWeekendId(1)).thenReturn(Collections.emptyList()); + + String accJson = """ + { + "entries": [ + { + "drivers": [ + { + "firstName": "Max", + "lastName": "Mustermann", + "shortName": "MUS", + "driverCategory": 1, + "playerID": "S76561198000000001" + } + ], + "raceNumber": 1, + "forcedCarModel": 35, + "defaultGridPosition": -1, + "overrideDriverInfo": 0 + }, + { + "drivers": [ + { + "firstName": "John", + "lastName": "Doe", + "shortName": "DOE", + "driverCategory": 2, + "playerID": "S76561198000000002" + }, + { + "firstName": "Jane", + "lastName": "Smith", + "shortName": "SMI", + "driverCategory": 1, + "playerID": "S76561198000000003" + } + ], + "raceNumber": 42, + "forcedCarModel": 30 + } + ], + "forceEntryList": 1 + } + """; + + entrylistService.uploadEntrylist(1, accJson); + + verify(entrylistMapper).deleteByRaceWeekendId(1); + verify(entrylistMapper).insert(any(StewardingEntrylist.class)); + verify(entrylistEntryMapper, times(2)).insert(any(StewardingEntrylistEntry.class)); + verify(entrylistDriverMapper, times(3)).insert(any(StewardingEntrylistDriver.class)); + } + + @Test + void uploadEntrylist_shouldHandleEmptyEntries() { + when(entrylistMapper.findByRaceWeekendId(1)).thenReturn(Collections.emptyList()); + + String accJson = """ + { + "entries": [], + "forceEntryList": 1 + } + """; + + entrylistService.uploadEntrylist(1, accJson); + + verify(entrylistMapper).deleteByRaceWeekendId(1); + verify(entrylistMapper).insert(any(StewardingEntrylist.class)); + verify(entrylistEntryMapper, never()).insert(any()); + verify(entrylistDriverMapper, never()).insert(any()); + } +} From fd5a6bc9737a63ebbedee753668f094101783eb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:43:37 +0000 Subject: [PATCH 09/24] Replace /new form views with dialogs, add STEWARD/DRIVER roles, make grid formatting consistent Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../simdesk/entities/auth/UserRoleEnum.java | 4 +- .../simdesk/services/MenuService.java | 2 +- .../views/stewarding/IncidentDetailView.java | 2 +- .../stewarding/PenaltyCatalogDetailView.java | 68 ++-------- .../stewarding/PenaltyCatalogListView.java | 62 ++++++++- .../stewarding/RaceWeekendDetailView.java | 36 +++-- .../views/stewarding/RaceWeekendFormView.java | 125 ------------------ .../views/stewarding/RaceWeekendListView.java | 110 +++++++++++++-- .../RaceWeekendSessionDetailView.java | 14 +- .../stewarding/ReasoningTemplateListView.java | 10 +- .../stewarding/StewardingTrackFormView.java | 90 ------------- .../stewarding/StewardingTrackListView.java | 69 +++++++++- .../postgres/V2_12_0__stewarding_roles.sql | 5 + .../sqlite/V2_12_0__stewarding_roles.sql | 5 + 14 files changed, 287 insertions(+), 315 deletions(-) delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java create mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql create mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/auth/UserRoleEnum.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/auth/UserRoleEnum.java index 6ceea4e9..8d4b1ac2 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/auth/UserRoleEnum.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/auth/UserRoleEnum.java @@ -2,5 +2,7 @@ public enum UserRoleEnum { ROLE_ADMIN, - ROLE_MANAGER + ROLE_MANAGER, + ROLE_STEWARD, + ROLE_DRIVER } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java index 5765fccd..9cfc2ddf 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java @@ -59,7 +59,7 @@ public List getItems() { if (SpringProfile.isStewardingEnabled()) { items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Race Weekends", VaadinIcon.CALENDAR, RaceWeekendListView.class)); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Tracks", VaadinIcon.ROAD, StewardingTrackListView.class)); items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Penalty Catalogs", VaadinIcon.BOOK, PenaltyCatalogListView.class)); items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Templates", VaadinIcon.FILE_TEXT_O, ReasoningTemplateListView.class)); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index de25d2ea..5bf3a1ef 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -121,7 +121,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(detailsLayout); // Steward Decision section (ADMIN only) - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { add(createDecisionSection(incident, weekend)); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java index 55736ec6..2635a03a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java @@ -6,6 +6,7 @@ import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; @@ -29,7 +30,7 @@ @Profile(SpringProfile.STEWARDING) @Route(value = "/stewarding/catalogs/:catalogId", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) +@RolesAllowed({"ADMIN", "STEWARD"}) public class PenaltyCatalogDetailView extends BaseView { private final PenaltyCatalogService catalogService; @@ -55,13 +56,6 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - // Handle "new" catalog creation - if ("new".equals(catalogIdParam)) { - add(createViewHeader("New Penalty Catalog")); - add(createNewCatalogForm()); - return; - } - Integer catalogId; try { catalogId = Integer.valueOf(catalogIdParam); @@ -94,62 +88,22 @@ public void beforeEnter(BeforeEnterEvent event) { List definitions = catalogService.getDefinitionsByCatalogId(catalogId); Grid grid = new Grid<>(PenaltyDefinition.class, false); - grid.addColumn(PenaltyDefinition::getCode).setHeader("Code").setAutoWidth(true); - grid.addColumn(PenaltyDefinition::getName).setHeader("Name").setAutoWidth(true); - grid.addColumn(PenaltyDefinition::getCategory).setHeader("Category").setAutoWidth(true); + grid.addColumn(PenaltyDefinition::getCode).setHeader("Code").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(PenaltyDefinition::getName).setHeader("Name").setSortable(true); + grid.addColumn(PenaltyDefinition::getCategory).setHeader("Category").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.addColumn(def -> def.getSessionType() != null ? def.getSessionType().getDescription() : "-") - .setHeader("Session Type").setAutoWidth(true); - grid.addColumn(PenaltyDefinition::getDefaultPenalty).setHeader("Default Penalty").setAutoWidth(true); - grid.addColumn(PenaltyDefinition::getSeverity).setHeader("Severity").setAutoWidth(true); + .setHeader("Session Type").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(PenaltyDefinition::getDefaultPenalty).setHeader("Default Penalty").setAutoWidth(true).setFlexGrow(0); + grid.addColumn(PenaltyDefinition::getSeverity).setHeader("Severity").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(definitions); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); addAndExpand(grid); } - private FormLayout createNewCatalogForm() { - FormLayout form = new FormLayout(); - form.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - - TextField nameField = new TextField("Name"); - nameField.setRequired(true); - nameField.setWidthFull(); - - TextField descriptionField = new TextField("Description"); - descriptionField.setWidthFull(); - - form.add(nameField, descriptionField); - - Button saveButton = new Button("Save", e -> { - if (nameField.isEmpty()) { - Notification.show("Name is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - PenaltyCatalog catalog = PenaltyCatalog.builder() - .name(nameField.getValue()) - .description(descriptionField.getValue()) - .build(); - catalogService.createCatalog(catalog); - Notification.show("Catalog created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> - getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class))); - - HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); - form.add(buttonLayout, 2); - - return form; - } - private void openPenaltyDialog(Integer catalogId) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Add Penalty Definition"); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java index db8c2d9b..94bb4ff8 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java @@ -2,8 +2,14 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParameters; @@ -19,7 +25,7 @@ @Profile(SpringProfile.STEWARDING) @Route(value = "/stewarding/catalogs", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) +@RolesAllowed({"ADMIN", "STEWARD"}) public class PenaltyCatalogListView extends BaseView { private final PenaltyCatalogService catalogService; @@ -44,9 +50,7 @@ public void beforeEnter(BeforeEnterEvent event) { headerLayout.setAlignItems(Alignment.CENTER); headerLayout.add(createViewHeader()); - Button newButton = new Button("New Catalog", e -> - getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogDetailView.class, - new RouteParameters("catalogId", "new")))); + Button newButton = new Button("New Catalog", e -> openNewCatalogDialog()); newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); headerLayout.add(newButton); @@ -54,10 +58,13 @@ public void beforeEnter(BeforeEnterEvent event) { List catalogs = catalogService.getAllCatalogs(); Grid grid = new Grid<>(PenaltyCatalog.class, false); - grid.addColumn(PenaltyCatalog::getName).setHeader("Name").setAutoWidth(true); - grid.addColumn(PenaltyCatalog::getDescription).setHeader("Description").setAutoWidth(true); + grid.addColumn(PenaltyCatalog::getName).setHeader("Name").setSortable(true); + grid.addColumn(PenaltyCatalog::getDescription).setHeader("Description").setSortable(true); grid.setItems(catalogs); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addItemClickListener(e -> getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogDetailView.class, new RouteParameters("catalogId", String.valueOf(e.getItem().getId())))) @@ -65,4 +72,47 @@ public void beforeEnter(BeforeEnterEvent event) { addAndExpand(grid); } + + private void openNewCatalogDialog() { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("New Penalty Catalog"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); + + TextField nameField = new TextField("Name"); + nameField.setRequired(true); + nameField.setWidthFull(); + + TextField descriptionField = new TextField("Description"); + descriptionField.setWidthFull(); + + form.add(nameField, descriptionField); + + Button saveButton = new Button("Save", e -> { + if (nameField.isEmpty()) { + Notification.show("Name is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + PenaltyCatalog catalog = PenaltyCatalog.builder() + .name(nameField.getValue()) + .description(descriptionField.getValue()) + .build(); + catalogService.createCatalog(catalog); + dialog.close(); + Notification.show("Catalog created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java index 827083cf..5740dcdf 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java @@ -3,6 +3,7 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.notification.Notification; @@ -111,7 +112,7 @@ private VerticalLayout createSessionsTab(Integer weekendId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { Button addSessionButton = new Button("Add Session"); addSessionButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); layout.add(addSessionButton); @@ -120,12 +121,15 @@ private VerticalLayout createSessionsTab(Integer weekendId) { List sessions = raceWeekendService.getSessionsByWeekendId(weekendId); Grid grid = new Grid<>(RaceWeekendSession.class, false); grid.addColumn(session -> session.getSessionType() != null ? session.getSessionType().getDescription() : "-") - .setHeader("Type").setAutoWidth(true); - grid.addColumn(RaceWeekendSession::getTitle).setHeader("Title").setAutoWidth(true); - grid.addColumn(RaceWeekendSession::getStartTime).setHeader("Start Time").setAutoWidth(true); - grid.addColumn(RaceWeekendSession::getEndTime).setHeader("End Time").setAutoWidth(true); + .setHeader("Type").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(RaceWeekendSession::getTitle).setHeader("Title").setSortable(true); + grid.addColumn(RaceWeekendSession::getStartTime).setHeader("Start Time").setAutoWidth(true).setFlexGrow(0); + grid.addColumn(RaceWeekendSession::getEndTime).setHeader("End Time").setAutoWidth(true).setFlexGrow(0); grid.setItems(sessions); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addItemClickListener(e -> getUI().ifPresent(ui -> ui.navigate(RaceWeekendSessionDetailView.class, new RouteParameters( @@ -152,13 +156,16 @@ private VerticalLayout createIncidentsTab(Integer weekendId) { grid.addColumn(incident -> { RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); return session != null ? session.getTitle() : "-"; - }).setHeader("Session").setAutoWidth(true); - grid.addColumn(Incident::getTitle).setHeader("Title").setAutoWidth(true); + }).setHeader("Session").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Incident::getTitle).setHeader("Title").setSortable(true); grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") - .setHeader("Status").setAutoWidth(true); - grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true); + .setHeader("Status").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(allIncidents); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); layout.addAndExpand(grid); return layout; @@ -168,7 +175,7 @@ private VerticalLayout createEntrylistTab(Integer weekendId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { Upload upload = new Upload(); upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { String json = new String(data, StandardCharsets.UTF_8); @@ -195,11 +202,14 @@ private VerticalLayout createEntrylistTab(Integer weekendId) { if (entrylist != null) { List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); Grid grid = new Grid<>(StewardingEntrylistEntry.class, false); - grid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true); - grid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setAutoWidth(true); - grid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setAutoWidth(true); + grid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setSortable(true); + grid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setSortable(true); grid.setItems(entries); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); layout.addAndExpand(grid); } else { layout.add(new H3("No entrylist uploaded yet")); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java deleted file mode 100644 index 8b021a63..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendFormView.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.sustineo.simdesk.views.stewarding; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.datepicker.DatePicker; -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.textfield.TextArea; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.Route; -import de.sustineo.simdesk.configuration.SpringProfile; -import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; -import de.sustineo.simdesk.entities.stewarding.RaceWeekend; -import de.sustineo.simdesk.entities.stewarding.StewardingTrack; -import de.sustineo.simdesk.layouts.MainLayout; -import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; -import de.sustineo.simdesk.services.stewarding.RaceWeekendService; -import de.sustineo.simdesk.services.stewarding.StewardingTrackService; -import de.sustineo.simdesk.views.BaseView; -import jakarta.annotation.security.RolesAllowed; -import org.springframework.context.annotation.Profile; - -@Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/weekends/new", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) -public class RaceWeekendFormView extends BaseView { - private final RaceWeekendService raceWeekendService; - private final StewardingTrackService trackService; - private final PenaltyCatalogService catalogService; - - public RaceWeekendFormView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, PenaltyCatalogService catalogService) { - this.raceWeekendService = raceWeekendService; - this.trackService = trackService; - this.catalogService = catalogService; - } - - @Override - public String getPageTitle() { - return "New Race Weekend"; - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - setSizeFull(); - setPadding(false); - setSpacing(false); - removeAll(); - - add(createViewHeader()); - - TextField titleField = new TextField("Title"); - titleField.setWidthFull(); - titleField.setRequired(true); - - TextArea descriptionField = new TextArea("Description"); - descriptionField.setWidthFull(); - - ComboBox trackCombo = new ComboBox<>("Track"); - trackCombo.setItems(trackService.getAllTracks()); - trackCombo.setItemLabelGenerator(StewardingTrack::getName); - trackCombo.setWidthFull(); - - ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); - catalogCombo.setItems(catalogService.getAllCatalogs()); - catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); - catalogCombo.setWidthFull(); - - TextField webhookField = new TextField("Discord Webhook URL"); - webhookField.setWidthFull(); - - DatePicker startDatePicker = new DatePicker("Start Date"); - startDatePicker.setWidthFull(); - - DatePicker endDatePicker = new DatePicker("End Date"); - endDatePicker.setWidthFull(); - - FormLayout formLayout = new FormLayout(); - formLayout.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - formLayout.add(titleField, 2); - formLayout.add(descriptionField, 2); - formLayout.add(trackCombo); - formLayout.add(catalogCombo); - formLayout.add(webhookField, 2); - formLayout.add(startDatePicker); - formLayout.add(endDatePicker); - - Button saveButton = new Button("Save", e -> { - if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - RaceWeekend weekend = RaceWeekend.builder() - .title(titleField.getValue()) - .description(descriptionField.getValue()) - .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) - .penaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null) - .discordWebhookUrl(webhookField.getValue()) - .startDate(startDatePicker.getValue()) - .endDate(endDatePicker.getValue()) - .build(); - - raceWeekendService.createWeekend(weekend); - Notification.show("Race weekend created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class))); - - HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); - - add(formLayout, buttonLayout); - } -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java index 910f5f21..95b38dab 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java @@ -2,17 +2,29 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.server.auth.AnonymousAllowed; import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; import de.sustineo.simdesk.entities.stewarding.RaceWeekend; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; import de.sustineo.simdesk.layouts.MainLayout; import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.services.stewarding.RaceWeekendService; import de.sustineo.simdesk.services.stewarding.StewardingTrackService; import de.sustineo.simdesk.views.BaseView; @@ -26,11 +38,14 @@ public class RaceWeekendListView extends BaseView { private final RaceWeekendService raceWeekendService; private final StewardingTrackService trackService; + private final PenaltyCatalogService catalogService; private final SecurityService securityService; - public RaceWeekendListView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, SecurityService securityService) { + public RaceWeekendListView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, + PenaltyCatalogService catalogService, SecurityService securityService) { this.raceWeekendService = raceWeekendService; this.trackService = trackService; + this.catalogService = catalogService; this.securityService = securityService; } @@ -51,9 +66,8 @@ public void beforeEnter(BeforeEnterEvent event) { headerLayout.setAlignItems(Alignment.CENTER); headerLayout.add(createViewHeader()); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { - Button newButton = new Button("New Weekend", e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendFormView.class))); + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + Button newButton = new Button("New Weekend", e -> openNewWeekendDialog()); newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); headerLayout.add(newButton); } @@ -62,15 +76,18 @@ public void beforeEnter(BeforeEnterEvent event) { List weekends = raceWeekendService.getAllWeekends(); Grid grid = new Grid<>(RaceWeekend.class, false); - grid.addColumn(RaceWeekend::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(RaceWeekend::getTitle).setHeader("Title").setSortable(true); grid.addColumn(weekend -> { var track = trackService.getTrackById(weekend.getTrackId()); return track != null ? track.getName() : "-"; - }).setHeader("Track").setAutoWidth(true); - grid.addColumn(RaceWeekend::getStartDate).setHeader("Start Date").setAutoWidth(true); - grid.addColumn(RaceWeekend::getEndDate).setHeader("End Date").setAutoWidth(true); + }).setHeader("Track").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(RaceWeekend::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(RaceWeekend::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(weekends); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addItemClickListener(e -> getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, new RouteParameters("weekendId", String.valueOf(e.getItem().getId())))) @@ -78,4 +95,81 @@ public void beforeEnter(BeforeEnterEvent event) { addAndExpand(grid); } + + private void openNewWeekendDialog() { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("New Race Weekend"); + dialog.setWidth("700px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + + ComboBox trackCombo = new ComboBox<>("Track"); + trackCombo.setItems(trackService.getAllTracks()); + trackCombo.setItemLabelGenerator(StewardingTrack::getName); + trackCombo.setWidthFull(); + + ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); + catalogCombo.setItems(catalogService.getAllCatalogs()); + catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); + catalogCombo.setWidthFull(); + + TextField webhookField = new TextField("Discord Webhook URL"); + webhookField.setWidthFull(); + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + + form.add(titleField, 2); + form.add(descriptionField, 2); + form.add(trackCombo); + form.add(catalogCombo); + form.add(webhookField, 2); + form.add(startDatePicker); + form.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + RaceWeekend weekend = RaceWeekend.builder() + .title(titleField.getValue()) + .description(descriptionField.getValue()) + .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) + .penaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null) + .discordWebhookUrl(webhookField.getValue()) + .startDate(startDatePicker.getValue()) + .endDate(endDatePicker.getValue()) + .build(); + + raceWeekendService.createWeekend(weekend); + dialog.close(); + Notification.show("Race weekend created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java index 048a0320..2815acca 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java @@ -3,6 +3,7 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; @@ -92,7 +93,7 @@ public void beforeEnter(BeforeEnterEvent event) { } add(infoLayout); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN)) { + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { HorizontalLayout actionLayout = new HorizontalLayout(); Button reportIncidentButton = new Button("Report Incident"); reportIncidentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -104,13 +105,16 @@ public void beforeEnter(BeforeEnterEvent event) { List incidents = incidentService.getIncidentsBySessionId(sessionId); Grid grid = new Grid<>(Incident.class, false); - grid.addColumn(Incident::getTitle).setHeader("Title").setAutoWidth(true); + grid.addColumn(Incident::getTitle).setHeader("Title").setSortable(true); grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") - .setHeader("Status").setAutoWidth(true); - grid.addColumn(Incident::getInvolvedCarsText).setHeader("Involved Cars").setAutoWidth(true); - grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true); + .setHeader("Status").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Incident::getInvolvedCarsText).setHeader("Involved Cars").setAutoWidth(true).setFlexGrow(0); + grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(incidents); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addItemClickListener(e -> getUI().ifPresent(ui -> ui.navigate(IncidentDetailView.class, new RouteParameters( diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java index eb4fc484..d9555398 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java @@ -5,6 +5,7 @@ import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; @@ -24,7 +25,7 @@ @Profile(SpringProfile.STEWARDING) @Route(value = "/stewarding/templates", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) +@RolesAllowed({"ADMIN", "STEWARD"}) public class ReasoningTemplateListView extends BaseView { private final ReasoningTemplateService templateService; @@ -57,8 +58,8 @@ public void beforeEnter(BeforeEnterEvent event) { List templates = templateService.getAllTemplates(); Grid grid = new Grid<>(ReasoningTemplate.class, false); - grid.addColumn(ReasoningTemplate::getName).setHeader("Name").setAutoWidth(true); - grid.addColumn(ReasoningTemplate::getCategory).setHeader("Category").setAutoWidth(true); + grid.addColumn(ReasoningTemplate::getName).setHeader("Name").setSortable(true); + grid.addColumn(ReasoningTemplate::getCategory).setHeader("Category").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.addColumn(template -> { String text = template.getTemplateText(); if (text != null && text.length() > 100) { @@ -68,6 +69,9 @@ public void beforeEnter(BeforeEnterEvent event) { }).setHeader("Template Text").setAutoWidth(true); grid.setItems(templates); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); addAndExpand(grid); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java deleted file mode 100644 index 025b6ce7..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackFormView.java +++ /dev/null @@ -1,90 +0,0 @@ -package de.sustineo.simdesk.views.stewarding; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.Route; -import de.sustineo.simdesk.configuration.SpringProfile; -import de.sustineo.simdesk.entities.stewarding.StewardingTrack; -import de.sustineo.simdesk.layouts.MainLayout; -import de.sustineo.simdesk.services.stewarding.StewardingTrackService; -import de.sustineo.simdesk.views.BaseView; -import jakarta.annotation.security.RolesAllowed; -import org.springframework.context.annotation.Profile; - -@Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/tracks/new", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) -public class StewardingTrackFormView extends BaseView { - private final StewardingTrackService trackService; - - public StewardingTrackFormView(StewardingTrackService trackService) { - this.trackService = trackService; - } - - @Override - public String getPageTitle() { - return "New Track"; - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - setSizeFull(); - setPadding(false); - setSpacing(false); - removeAll(); - - add(createViewHeader()); - - TextField nameField = new TextField("Name"); - nameField.setWidthFull(); - nameField.setRequired(true); - - TextField countryField = new TextField("Country"); - countryField.setWidthFull(); - - TextField mapImageUrlField = new TextField("Map Image URL"); - mapImageUrlField.setWidthFull(); - - FormLayout formLayout = new FormLayout(); - formLayout.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - formLayout.add(nameField); - formLayout.add(countryField); - formLayout.add(mapImageUrlField, 2); - - Button saveButton = new Button("Save", e -> { - if (nameField.isEmpty()) { - Notification.show("Name is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - StewardingTrack track = StewardingTrack.builder() - .name(nameField.getValue()) - .country(countryField.getValue()) - .mapImageUrl(mapImageUrlField.getValue()) - .build(); - - trackService.createTrack(track); - Notification.show("Track created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.navigate(StewardingTrackListView.class)); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> - getUI().ifPresent(ui -> ui.navigate(StewardingTrackListView.class))); - - HorizontalLayout buttonLayout = new HorizontalLayout(saveButton, cancelButton); - - add(formLayout, buttonLayout); - } -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java index cd52bcdd..4970f118 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java @@ -2,8 +2,14 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import de.sustineo.simdesk.configuration.SpringProfile; @@ -18,7 +24,7 @@ @Profile(SpringProfile.STEWARDING) @Route(value = "/stewarding/tracks", layout = MainLayout.class) -@RolesAllowed({"ADMIN"}) +@RolesAllowed({"ADMIN", "STEWARD"}) public class StewardingTrackListView extends BaseView { private final StewardingTrackService trackService; @@ -43,8 +49,7 @@ public void beforeEnter(BeforeEnterEvent event) { headerLayout.setAlignItems(Alignment.CENTER); headerLayout.add(createViewHeader()); - Button newButton = new Button("New Track", e -> - getUI().ifPresent(ui -> ui.navigate(StewardingTrackFormView.class))); + Button newButton = new Button("New Track", e -> openNewTrackDialog()); newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); headerLayout.add(newButton); @@ -52,11 +57,65 @@ public void beforeEnter(BeforeEnterEvent event) { List tracks = trackService.getAllTracks(); Grid grid = new Grid<>(StewardingTrack.class, false); - grid.addColumn(StewardingTrack::getName).setHeader("Name").setAutoWidth(true); - grid.addColumn(StewardingTrack::getCountry).setHeader("Country").setAutoWidth(true); + grid.addColumn(StewardingTrack::getName).setHeader("Name").setSortable(true); + grid.addColumn(StewardingTrack::getCountry).setHeader("Country").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(tracks); grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); addAndExpand(grid); } + + private void openNewTrackDialog() { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("New Track"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField nameField = new TextField("Name"); + nameField.setRequired(true); + nameField.setWidthFull(); + + TextField countryField = new TextField("Country"); + countryField.setWidthFull(); + + TextField mapImageUrlField = new TextField("Map Image URL"); + mapImageUrlField.setWidthFull(); + + form.add(nameField, countryField, mapImageUrlField); + + Button saveButton = new Button("Save", e -> { + if (nameField.isEmpty()) { + Notification.show("Name is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + StewardingTrack track = StewardingTrack.builder() + .name(nameField.getValue()) + .country(countryField.getValue()) + .mapImageUrl(mapImageUrlField.getValue()) + .build(); + + trackService.createTrack(track); + dialog.close(); + Notification.show("Track created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } } diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql new file mode 100644 index 00000000..80e160f8 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql @@ -0,0 +1,5 @@ +INSERT INTO simdesk.user_role (name, description) +VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); + +INSERT INTO simdesk.user_role (name, description) +VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql new file mode 100644 index 00000000..9fe375c9 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql @@ -0,0 +1,5 @@ +INSERT INTO user_role (name, description) +VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); + +INSERT INTO user_role (name, description) +VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); From 3175867a512f710905fc2db0c2cfd7b614adf9dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:26:00 +0000 Subject: [PATCH 10/24] Fix Add Session dialog, allow drivers to report incidents, enhance description formatting Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../views/stewarding/IncidentDetailView.java | 28 ++++- .../stewarding/RaceWeekendDetailView.java | 94 ++++++++++++++- .../RaceWeekendSessionDetailView.java | 113 ++++++++++++++++-- 3 files changed, 218 insertions(+), 17 deletions(-) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index 5bf3a1ef..7fe9dd6f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -101,23 +101,29 @@ public void beforeEnter(BeforeEnterEvent event) { detailsLayout.setSpacing(false); if (incident.getDescription() != null && !incident.getDescription().isEmpty()) { - detailsLayout.add(new Paragraph(incident.getDescription())); + detailsLayout.add(new Span(incident.getDescription())); } if (incident.getLap() != null) { - detailsLayout.add(new Paragraph("Lap: " + incident.getLap())); + detailsLayout.add(createDetailRow("Lap", String.valueOf(incident.getLap()))); } if (incident.getTimestampInSession() != null) { - detailsLayout.add(new Paragraph("Time in Session: " + incident.getTimestampInSession())); + detailsLayout.add(createDetailRow("Time in Session", incident.getTimestampInSession())); } if (incident.getInvolvedCarsText() != null) { - detailsLayout.add(new Paragraph("Involved Cars: " + incident.getInvolvedCarsText())); + detailsLayout.add(createDetailRow("Involved Cars", incident.getInvolvedCarsText())); } if (incident.getVideoUrl() != null && !incident.getVideoUrl().isEmpty()) { Anchor videoLink = new Anchor(incident.getVideoUrl(), "Video Evidence"); videoLink.setTarget("_blank"); - detailsLayout.add(videoLink); + HorizontalLayout videoRow = new HorizontalLayout(); + Span videoLabel = new Span("Evidence: "); + videoLabel.getStyle().set("font-weight", "bold"); + videoRow.setSpacing(false); + videoRow.getStyle().set("gap", "var(--lumo-space-xs)"); + videoRow.add(videoLabel, videoLink); + detailsLayout.add(videoRow); } - detailsLayout.add(new Paragraph("Status: " + (incident.getStatus() != null ? incident.getStatus().getDescription() : "-"))); + detailsLayout.add(createDetailRow("Status", incident.getStatus() != null ? incident.getStatus().getDescription() : "-")); add(detailsLayout); // Steward Decision section (ADMIN only) @@ -281,4 +287,14 @@ private VerticalLayout createAppealsSection(Integer incidentId) { return layout; } + + private HorizontalLayout createDetailRow(String label, String value) { + Span labelSpan = new Span(label + ": "); + labelSpan.getStyle().set("font-weight", "bold"); + Span valueSpan = new Span(value); + HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); + row.setSpacing(false); + row.getStyle().set("gap", "var(--lumo-space-xs)"); + return row; + } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java index 5740dcdf..d675995b 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java @@ -2,15 +2,21 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.datetimepicker.DateTimePicker; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.H3; -import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.tabs.TabSheet; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.server.streams.UploadHandler; import com.vaadin.flow.router.BeforeEnterEvent; @@ -30,6 +36,7 @@ import org.springframework.context.annotation.Profile; import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; @@ -88,13 +95,16 @@ public void beforeEnter(BeforeEnterEvent event) { infoLayout.setPadding(true); infoLayout.setSpacing(false); if (weekend.getDescription() != null && !weekend.getDescription().isEmpty()) { - infoLayout.add(new Paragraph(weekend.getDescription())); + infoLayout.add(new Span(weekend.getDescription())); } if (weekend.getTrack() != null) { - infoLayout.add(new Paragraph("Track: " + weekend.getTrack().getName())); + infoLayout.add(createDetailRow("Track", weekend.getTrack().getName())); + } + if (weekend.getPenaltyCatalog() != null) { + infoLayout.add(createDetailRow("Penalty Catalog", weekend.getPenaltyCatalog().getName())); } if (weekend.getStartDate() != null && weekend.getEndDate() != null) { - infoLayout.add(new Paragraph("Date: " + weekend.getStartDate() + " — " + weekend.getEndDate())); + infoLayout.add(createDetailRow("Date", weekend.getStartDate() + " — " + weekend.getEndDate())); } add(infoLayout); @@ -108,6 +118,16 @@ public void beforeEnter(BeforeEnterEvent event) { addAndExpand(tabSheet); } + private HorizontalLayout createDetailRow(String label, String value) { + Span labelSpan = new Span(label + ": "); + labelSpan.getStyle().set("font-weight", "bold"); + Span valueSpan = new Span(value); + HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); + row.setSpacing(false); + row.getStyle().set("gap", "var(--lumo-space-xs)"); + return row; + } + private VerticalLayout createSessionsTab(Integer weekendId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); @@ -115,6 +135,7 @@ private VerticalLayout createSessionsTab(Integer weekendId) { if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { Button addSessionButton = new Button("Add Session"); addSessionButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + addSessionButton.addClickListener(e -> openAddSessionDialog(weekendId)); layout.add(addSessionButton); } @@ -142,6 +163,71 @@ private VerticalLayout createSessionsTab(Integer weekendId) { return layout; } + private void openAddSessionDialog(Integer weekendId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Add Session"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + ComboBox typeCombo = new ComboBox<>("Session Type"); + typeCombo.setItems(StewSessionType.values()); + typeCombo.setItemLabelGenerator(StewSessionType::getDescription); + typeCombo.setRequired(true); + typeCombo.setWidthFull(); + + TextField titleField = new TextField("Title"); + titleField.setRequired(true); + titleField.setWidthFull(); + + DateTimePicker startTimePicker = new DateTimePicker("Start Time"); + startTimePicker.setWidthFull(); + + DateTimePicker endTimePicker = new DateTimePicker("End Time"); + endTimePicker.setWidthFull(); + + IntegerField sortOrderField = new IntegerField("Sort Order"); + sortOrderField.setMin(0); + sortOrderField.setValue(0); + sortOrderField.setWidthFull(); + + form.add(typeCombo, titleField, startTimePicker, endTimePicker, sortOrderField); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty() || typeCombo.isEmpty()) { + Notification.show("Session type and title are required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + RaceWeekendSession session = RaceWeekendSession.builder() + .raceWeekendId(weekendId) + .sessionType(typeCombo.getValue()) + .title(titleField.getValue()) + .startTime(startTimePicker.getValue() != null ? startTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) + .endTime(endTimePicker.getValue() != null ? endTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) + .sortOrder(sortOrderField.getValue()) + .build(); + + raceWeekendService.createSession(session); + dialog.close(); + Notification.show("Session created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } + private VerticalLayout createIncidentsTab(Integer weekendId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java index 2815acca..ec8737d5 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java @@ -2,11 +2,18 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParam; @@ -15,6 +22,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.auth.UserRoleEnum; import de.sustineo.simdesk.entities.stewarding.Incident; +import de.sustineo.simdesk.entities.stewarding.IncidentStatus; import de.sustineo.simdesk.entities.stewarding.RaceWeekend; import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; import de.sustineo.simdesk.layouts.MainLayout; @@ -83,23 +91,32 @@ public void beforeEnter(BeforeEnterEvent event) { infoLayout.setPadding(true); infoLayout.setSpacing(false); if (session.getSessionType() != null) { - infoLayout.add(new Paragraph("Type: " + session.getSessionType().getDescription())); + infoLayout.add(createDetailRow("Type", session.getSessionType().getDescription())); } if (session.getStartTime() != null) { - infoLayout.add(new Paragraph("Start: " + session.getStartTime())); + infoLayout.add(createDetailRow("Start", session.getStartTime().toString())); } if (session.getEndTime() != null) { - infoLayout.add(new Paragraph("End: " + session.getEndTime())); + infoLayout.add(createDetailRow("End", session.getEndTime().toString())); } add(infoLayout); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - HorizontalLayout actionLayout = new HorizontalLayout(); + HorizontalLayout actionLayout = new HorizontalLayout(); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD, UserRoleEnum.ROLE_DRIVER)) { Button reportIncidentButton = new Button("Report Incident"); reportIncidentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + reportIncidentButton.addClickListener(e -> openReportIncidentDialog(sessionId, weekendId)); + actionLayout.add(reportIncidentButton); + } + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { Button quickDecisionButton = new Button("Quick Decision"); quickDecisionButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); - actionLayout.add(reportIncidentButton, quickDecisionButton); + actionLayout.add(quickDecisionButton); + } + + if (actionLayout.getComponentCount() > 0) { add(actionLayout); } @@ -125,4 +142,86 @@ public void beforeEnter(BeforeEnterEvent event) { addAndExpand(grid); } + + private HorizontalLayout createDetailRow(String label, String value) { + Span labelSpan = new Span(label + ": "); + labelSpan.getStyle().set("font-weight", "bold"); + Span valueSpan = new Span(value); + HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); + row.setSpacing(false); + row.getStyle().set("gap", "var(--lumo-space-xs)"); + return row; + } + + private void openReportIncidentDialog(Integer sessionId, Integer weekendId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Report Incident"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setRequired(true); + titleField.setWidthFull(); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + descriptionField.setMinHeight("100px"); + + IntegerField lapField = new IntegerField("Lap"); + lapField.setMin(0); + lapField.setWidthFull(); + + TextField timestampField = new TextField("Time in Session"); + timestampField.setWidthFull(); + + TextField involvedCarsField = new TextField("Involved Cars"); + involvedCarsField.setWidthFull(); + involvedCarsField.setPlaceholder("e.g. #001, #042"); + + TextField videoUrlField = new TextField("Video URL"); + videoUrlField.setWidthFull(); + + form.add(titleField, 2); + form.add(descriptionField, 2); + form.add(lapField, timestampField); + form.add(involvedCarsField, 2); + form.add(videoUrlField, 2); + + Button saveButton = new Button("Report", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + Incident incident = Incident.builder() + .sessionId(sessionId) + .title(titleField.getValue()) + .description(descriptionField.getValue()) + .lap(lapField.getValue()) + .timestampInSession(timestampField.getValue()) + .involvedCarsText(involvedCarsField.getValue()) + .videoUrl(videoUrlField.getValue()) + .status(IncidentStatus.REPORTED) + .build(); + + incidentService.createIncident(incident, List.of()); + dialog.close(); + Notification.show("Incident reported", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } } From 0be4b5e9f455bd4b3090537fe3731a2a7dc16db5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:51:09 +0000 Subject: [PATCH 11/24] Update stewarding views: back nav, entrylist per session, edit/delete, map markers - RaceWeekendDetailView: Add back button, videoUrlEnabled display, edit/delete weekend dialogs, remove entrylist tab (moved to session level) - RaceWeekendSessionDetailView: Add back button, entrylist upload/display, MultiSelectComboBox for involved cars, map marker fields, conditional video URL, edit/delete session buttons - IncidentDetailView: Add back button, map marker coordinate display - PenaltyCatalogDetailView: Add back button - RaceWeekendListView: Add videoUrlEnabled checkbox to new weekend dialog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../entities/stewarding/RaceWeekend.java | 1 + .../stewarding/StewardingEntrylist.java | 1 + .../mybatis/mapper/RaceWeekendMapper.java | 7 +- .../mapper/StewardingEntrylistMapper.java | 12 +- .../StewardingEntrylistService.java | 53 +++++- .../views/stewarding/IncidentDetailView.java | 15 ++ .../stewarding/PenaltyCatalogDetailView.java | 5 + .../stewarding/RaceWeekendDetailView.java | 178 +++++++++++++----- .../views/stewarding/RaceWeekendListView.java | 4 + .../RaceWeekendSessionDetailView.java | 156 ++++++++++++--- ..._13_0__stewarding_entrylist_to_session.sql | 9 + ..._13_0__stewarding_entrylist_to_session.sql | 9 + 12 files changed, 364 insertions(+), 86 deletions(-) create mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql create mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java index 87fb6c3f..f2cf5448 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java @@ -21,6 +21,7 @@ public class RaceWeekend { private Integer trackId; private Integer penaltyCatalogId; private String discordWebhookUrl; + private Boolean videoUrlEnabled; private LocalDate startDate; private LocalDate endDate; private Instant createdAt; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java index 345f42e6..9cb0a4f0 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java @@ -15,6 +15,7 @@ public class StewardingEntrylist { private Integer id; private Integer raceWeekendId; + private Integer sessionId; private Instant uploadedAt; private String rawJson; } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java index 8f8cf070..dc313b18 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java @@ -16,6 +16,7 @@ public interface RaceWeekendMapper { @Result(property = "trackId", column = "track_id"), @Result(property = "penaltyCatalogId", column = "penalty_catalog_id"), @Result(property = "discordWebhookUrl", column = "discord_webhook_url"), + @Result(property = "videoUrlEnabled", column = "video_url_enabled"), @Result(property = "startDate", column = "start_date"), @Result(property = "endDate", column = "end_date"), @Result(property = "createdAt", column = "created_at"), @@ -33,8 +34,8 @@ public interface RaceWeekendMapper { List findByTrackId(Integer trackId); @Insert(""" - INSERT INTO stewarding_race_weekend (title, description, track_id, penalty_catalog_id, discord_webhook_url, start_date, end_date, created_at, updated_at) - VALUES (#{title}, #{description}, #{trackId}, #{penaltyCatalogId}, #{discordWebhookUrl}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO stewarding_race_weekend (title, description, track_id, penalty_catalog_id, discord_webhook_url, video_url_enabled, start_date, end_date, created_at, updated_at) + VALUES (#{title}, #{description}, #{trackId}, #{penaltyCatalogId}, #{discordWebhookUrl}, #{videoUrlEnabled}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(RaceWeekend weekend); @@ -42,7 +43,7 @@ INSERT INTO stewarding_race_weekend (title, description, track_id, penalty_catal @Update(""" UPDATE stewarding_race_weekend SET title = #{title}, description = #{description}, track_id = #{trackId}, penalty_catalog_id = #{penaltyCatalogId}, - discord_webhook_url = #{discordWebhookUrl}, start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP + discord_webhook_url = #{discordWebhookUrl}, video_url_enabled = #{videoUrlEnabled}, start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP WHERE id = #{id} """) void update(RaceWeekend weekend); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java index 9ca22dfb..6041bea6 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java @@ -12,23 +12,31 @@ public interface StewardingEntrylistMapper { @Results(id = "stewardingEntrylistResultMap", value = { @Result(id = true, property = "id", column = "id"), @Result(property = "raceWeekendId", column = "race_weekend_id"), + @Result(property = "sessionId", column = "session_id"), @Result(property = "uploadedAt", column = "uploaded_at"), @Result(property = "rawJson", column = "raw_json"), }) @Select("SELECT * FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") List findByRaceWeekendId(Integer raceWeekendId); + @ResultMap("stewardingEntrylistResultMap") + @Select("SELECT * FROM stewarding_entrylist WHERE session_id = #{sessionId}") + List findBySessionId(Integer sessionId); + @ResultMap("stewardingEntrylistResultMap") @Select("SELECT * FROM stewarding_entrylist WHERE id = #{id}") StewardingEntrylist findById(Integer id); @Insert(""" - INSERT INTO stewarding_entrylist (race_weekend_id, uploaded_at, raw_json) - VALUES (#{raceWeekendId}, CURRENT_TIMESTAMP, #{rawJson}) + INSERT INTO stewarding_entrylist (race_weekend_id, session_id, uploaded_at, raw_json) + VALUES (#{raceWeekendId}, #{sessionId}, CURRENT_TIMESTAMP, #{rawJson}) """) @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingEntrylist entrylist); @Delete("DELETE FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") void deleteByRaceWeekendId(Integer raceWeekendId); + + @Delete("DELETE FROM stewarding_entrylist WHERE session_id = #{sessionId}") + void deleteBySessionId(Integer sessionId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java index cdee93c7..65b43eda 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java @@ -30,6 +30,11 @@ public StewardingEntrylist getEntrylistByWeekendId(Integer weekendId) { return entrylists.isEmpty() ? null : entrylists.getFirst(); } + public StewardingEntrylist getEntrylistBySessionId(Integer sessionId) { + List entrylists = entrylistMapper.findBySessionId(sessionId); + return entrylists.isEmpty() ? null : entrylists.getFirst(); + } + public List getEntriesByEntrylistId(Integer entrylistId) { return entryMapper.findByEntrylistId(entrylistId); } @@ -54,6 +59,31 @@ public void uploadEntrylist(Integer weekendId, String jsonContent) { entrylist.setRawJson(jsonContent); entrylistMapper.insert(entrylist); + parseAndInsertEntries(root, entrylist); + } + + @Transactional + public void uploadEntrylistForSession(Integer sessionId, Integer weekendId, String jsonContent) { + deleteEntrylistForSession(sessionId); + + JsonNode root; + try { + root = objectMapper.readTree(jsonContent); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid JSON content", e); + } + + StewardingEntrylist entrylist = new StewardingEntrylist(); + entrylist.setSessionId(sessionId); + entrylist.setRaceWeekendId(weekendId); + entrylist.setRawJson(jsonContent); + entrylistMapper.insert(entrylist); + + parseAndInsertEntries(root, entrylist); + } + + private void parseAndInsertEntries(JsonNode root, StewardingEntrylist entrylist) { + JsonNode entries = root.get("entries"); if (entries == null || !entries.isArray()) { return; @@ -96,12 +126,25 @@ public void uploadEntrylist(Integer weekendId, String jsonContent) { public void deleteEntrylist(Integer weekendId) { List existing = entrylistMapper.findByRaceWeekendId(weekendId); for (StewardingEntrylist entrylist : existing) { - List entries = entryMapper.findByEntrylistId(entrylist.getId()); - for (StewardingEntrylistEntry entry : entries) { - driverMapper.deleteByEntryId(entry.getId()); - } - entryMapper.deleteByEntrylistId(entrylist.getId()); + deleteEntrylistEntries(entrylist); } entrylistMapper.deleteByRaceWeekendId(weekendId); } + + @Transactional + public void deleteEntrylistForSession(Integer sessionId) { + List existing = entrylistMapper.findBySessionId(sessionId); + for (StewardingEntrylist entrylist : existing) { + deleteEntrylistEntries(entrylist); + } + entrylistMapper.deleteBySessionId(sessionId); + } + + private void deleteEntrylistEntries(StewardingEntrylist entrylist) { + List entries = entryMapper.findByEntrylistId(entrylist.getId()); + for (StewardingEntrylistEntry entry : entries) { + driverMapper.deleteByEntryId(entry.getId()); + } + entryMapper.deleteByEntrylistId(entrylist.getId()); + } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index 7fe9dd6f..05023631 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -18,6 +18,8 @@ import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParam; +import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.server.auth.AnonymousAllowed; import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.auth.UserRoleEnum; @@ -95,6 +97,16 @@ public void beforeEnter(BeforeEnterEvent event) { add(createViewHeader(incident.getTitle())); + RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); + Button backButton = new Button("← Back to " + (session != null ? session.getTitle() : "Session"), e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendSessionDetailView.class, + new RouteParameters( + new RouteParam("weekendId", String.valueOf(weekendId)), + new RouteParam("sessionId", String.valueOf(incident.getSessionId())) + )))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + // Incident details VerticalLayout detailsLayout = new VerticalLayout(); detailsLayout.setPadding(true); @@ -109,6 +121,9 @@ public void beforeEnter(BeforeEnterEvent event) { if (incident.getTimestampInSession() != null) { detailsLayout.add(createDetailRow("Time in Session", incident.getTimestampInSession())); } + if (incident.getMapMarkerX() != null && incident.getMapMarkerY() != null) { + detailsLayout.add(createDetailRow("Map Location", String.format("X: %.2f, Y: %.2f", incident.getMapMarkerX(), incident.getMapMarkerY()))); + } if (incident.getInvolvedCarsText() != null) { detailsLayout.add(createDetailRow("Involved Cars", incident.getInvolvedCarsText())); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java index 2635a03a..6e19a5f2 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java @@ -72,6 +72,11 @@ public void beforeEnter(BeforeEnterEvent event) { add(createViewHeader(catalog.getName())); + Button backButton = new Button("← Back to Penalty Catalogs", e -> + getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + VerticalLayout infoLayout = new VerticalLayout(); infoLayout.setPadding(true); infoLayout.setSpacing(false); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java index d675995b..9817fd3a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java @@ -2,7 +2,10 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.datetimepicker.DateTimePicker; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.formlayout.FormLayout; @@ -16,9 +19,8 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.component.upload.Upload; -import com.vaadin.flow.server.streams.UploadHandler; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParam; @@ -29,13 +31,13 @@ import de.sustineo.simdesk.entities.stewarding.*; import de.sustineo.simdesk.layouts.MainLayout; import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.services.stewarding.RaceWeekendService; -import de.sustineo.simdesk.services.stewarding.StewardingEntrylistService; import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; import de.sustineo.simdesk.views.BaseView; import org.springframework.context.annotation.Profile; -import java.nio.charset.StandardCharsets; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; @@ -46,14 +48,17 @@ public class RaceWeekendDetailView extends BaseView { private final RaceWeekendService raceWeekendService; private final StewardingIncidentService incidentService; - private final StewardingEntrylistService entrylistService; + private final StewardingTrackService trackService; + private final PenaltyCatalogService catalogService; private final SecurityService securityService; public RaceWeekendDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, - StewardingEntrylistService entrylistService, SecurityService securityService) { + StewardingTrackService trackService, PenaltyCatalogService catalogService, + SecurityService securityService) { this.raceWeekendService = raceWeekendService; this.incidentService = incidentService; - this.entrylistService = entrylistService; + this.trackService = trackService; + this.catalogService = catalogService; this.securityService = securityService; } @@ -91,6 +96,11 @@ public void beforeEnter(BeforeEnterEvent event) { add(createViewHeader(weekend.getTitle())); + Button backButton = new Button("← Back to Race Weekends", e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + VerticalLayout infoLayout = new VerticalLayout(); infoLayout.setPadding(true); infoLayout.setSpacing(false); @@ -106,14 +116,40 @@ public void beforeEnter(BeforeEnterEvent event) { if (weekend.getStartDate() != null && weekend.getEndDate() != null) { infoLayout.add(createDetailRow("Date", weekend.getStartDate() + " — " + weekend.getEndDate())); } + infoLayout.add(createDetailRow("Video URL Enabled", Boolean.TRUE.equals(weekend.getVideoUrlEnabled()) ? "Yes" : "No")); add(infoLayout); + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + HorizontalLayout weekendActionLayout = new HorizontalLayout(); + Button editWeekendButton = new Button("Edit Weekend"); + editWeekendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + editWeekendButton.addClickListener(e -> openEditWeekendDialog(weekend)); + weekendActionLayout.add(editWeekendButton); + + Button deleteWeekendButton = new Button("Delete Weekend"); + deleteWeekendButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + deleteWeekendButton.addClickListener(e -> { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Delete Weekend"); + confirmDialog.setText("Are you sure you want to delete this weekend?"); + confirmDialog.setCancelable(true); + confirmDialog.setConfirmText("Delete"); + confirmDialog.setConfirmButtonTheme("error primary"); + confirmDialog.addConfirmListener(ev -> { + raceWeekendService.deleteWeekend(weekendId); + getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + }); + confirmDialog.open(); + }); + weekendActionLayout.add(deleteWeekendButton); + add(weekendActionLayout); + } + TabSheet tabSheet = new TabSheet(); tabSheet.setSizeFull(); tabSheet.add("Sessions", createSessionsTab(weekendId)); tabSheet.add("Incidents", createIncidentsTab(weekendId)); - tabSheet.add("Entrylist", createEntrylistTab(weekendId)); addAndExpand(tabSheet); } @@ -257,50 +293,96 @@ private VerticalLayout createIncidentsTab(Integer weekendId) { return layout; } - private VerticalLayout createEntrylistTab(Integer weekendId) { - VerticalLayout layout = new VerticalLayout(); - layout.setSizeFull(); + private void openEditWeekendDialog(RaceWeekend weekend) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Edit Race Weekend"); + dialog.setWidth("700px"); - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - Upload upload = new Upload(); - upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { - String json = new String(data, StandardCharsets.UTF_8); - try { - entrylistService.uploadEntrylist(weekendId, json); - getUI().ifPresent(ui -> ui.access(() -> { - Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - ui.getPage().reload(); - })); - } catch (IllegalArgumentException ex) { - getUI().ifPresent(ui -> ui.access(() -> - Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR) - )); - } - })); - upload.setAcceptedFileTypes("application/json", ".json"); - upload.setMaxFiles(1); - layout.add(upload); + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + titleField.setValue(weekend.getTitle() != null ? weekend.getTitle() : ""); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + descriptionField.setValue(weekend.getDescription() != null ? weekend.getDescription() : ""); + + ComboBox trackCombo = new ComboBox<>("Track"); + List tracks = trackService.getAllTracks(); + trackCombo.setItems(tracks); + trackCombo.setItemLabelGenerator(StewardingTrack::getName); + trackCombo.setWidthFull(); + if (weekend.getTrack() != null) { + tracks.stream().filter(t -> t.getId().equals(weekend.getTrackId())).findFirst().ifPresent(trackCombo::setValue); } - StewardingEntrylist entrylist = entrylistService.getEntrylistByWeekendId(weekendId); - if (entrylist != null) { - List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); - Grid grid = new Grid<>(StewardingEntrylistEntry.class, false); - grid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setSortable(true); - grid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setSortable(true); - grid.setItems(entries); - grid.setSizeFull(); - grid.setSelectionMode(Grid.SelectionMode.NONE); - grid.setColumnReorderingAllowed(true); - grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - layout.addAndExpand(grid); - } else { - layout.add(new H3("No entrylist uploaded yet")); + ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); + List catalogs = catalogService.getAllCatalogs(); + catalogCombo.setItems(catalogs); + catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); + catalogCombo.setWidthFull(); + if (weekend.getPenaltyCatalog() != null) { + catalogs.stream().filter(c -> c.getId().equals(weekend.getPenaltyCatalogId())).findFirst().ifPresent(catalogCombo::setValue); } - return layout; + TextField webhookField = new TextField("Discord Webhook URL"); + webhookField.setWidthFull(); + webhookField.setValue(weekend.getDiscordWebhookUrl() != null ? weekend.getDiscordWebhookUrl() : ""); + + Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); + videoUrlEnabledCheckbox.setValue(Boolean.TRUE.equals(weekend.getVideoUrlEnabled())); + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + startDatePicker.setValue(weekend.getStartDate()); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + endDatePicker.setValue(weekend.getEndDate()); + + form.add(titleField, 2); + form.add(descriptionField, 2); + form.add(trackCombo); + form.add(catalogCombo); + form.add(webhookField, 2); + form.add(videoUrlEnabledCheckbox, 2); + form.add(startDatePicker); + form.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + weekend.setTitle(titleField.getValue()); + weekend.setDescription(descriptionField.getValue()); + weekend.setTrackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null); + weekend.setPenaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null); + weekend.setDiscordWebhookUrl(webhookField.getValue()); + weekend.setVideoUrlEnabled(videoUrlEnabledCheckbox.getValue()); + weekend.setStartDate(startDatePicker.getValue()); + weekend.setEndDate(endDatePicker.getValue()); + + raceWeekendService.updateWeekend(weekend); + dialog.close(); + Notification.show("Weekend updated", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java index 95b38dab..dce9b2a9 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java @@ -2,6 +2,7 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.dialog.Dialog; @@ -138,6 +139,8 @@ private void openNewWeekendDialog() { form.add(trackCombo); form.add(catalogCombo); form.add(webhookField, 2); + Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); + form.add(videoUrlEnabledCheckbox, 2); form.add(startDatePicker); form.add(endDatePicker); @@ -154,6 +157,7 @@ private void openNewWeekendDialog() { .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) .penaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null) .discordWebhookUrl(webhookField.getValue()) + .videoUrlEnabled(videoUrlEnabledCheckbox.getValue()) .startDate(startDatePicker.getValue()) .endDate(endDatePicker.getValue()) .build(); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java index ec8737d5..b0a3ad45 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java @@ -2,18 +2,23 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.component.upload.Upload; +import com.vaadin.flow.server.streams.UploadHandler; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParam; @@ -21,18 +26,19 @@ import com.vaadin.flow.server.auth.AnonymousAllowed; import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.auth.UserRoleEnum; -import de.sustineo.simdesk.entities.stewarding.Incident; -import de.sustineo.simdesk.entities.stewarding.IncidentStatus; -import de.sustineo.simdesk.entities.stewarding.RaceWeekend; -import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; +import de.sustineo.simdesk.entities.stewarding.*; import de.sustineo.simdesk.layouts.MainLayout; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.RaceWeekendService; +import de.sustineo.simdesk.services.stewarding.StewardingEntrylistService; import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; import de.sustineo.simdesk.views.BaseView; import org.springframework.context.annotation.Profile; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Profile(SpringProfile.STEWARDING) @Route(value = "/stewarding/weekends/:weekendId/sessions/:sessionId", layout = MainLayout.class) @@ -40,12 +46,14 @@ public class RaceWeekendSessionDetailView extends BaseView { private final RaceWeekendService raceWeekendService; private final StewardingIncidentService incidentService; + private final StewardingEntrylistService entrylistService; private final SecurityService securityService; public RaceWeekendSessionDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, - SecurityService securityService) { + StewardingEntrylistService entrylistService, SecurityService securityService) { this.raceWeekendService = raceWeekendService; this.incidentService = incidentService; + this.entrylistService = entrylistService; this.securityService = securityService; } @@ -87,6 +95,12 @@ public void beforeEnter(BeforeEnterEvent event) { add(createViewHeader(session.getTitle())); + Button backButton = new Button("← Back to " + weekend.getTitle(), e -> + getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, + new RouteParameters("weekendId", String.valueOf(weekendId))))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + VerticalLayout infoLayout = new VerticalLayout(); infoLayout.setPadding(true); infoLayout.setSpacing(false); @@ -106,7 +120,7 @@ public void beforeEnter(BeforeEnterEvent event) { if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD, UserRoleEnum.ROLE_DRIVER)) { Button reportIncidentButton = new Button("Report Incident"); reportIncidentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - reportIncidentButton.addClickListener(e -> openReportIncidentDialog(sessionId, weekendId)); + reportIncidentButton.addClickListener(e -> openReportIncidentDialog(sessionId, weekendId, weekend)); actionLayout.add(reportIncidentButton); } @@ -114,6 +128,28 @@ public void beforeEnter(BeforeEnterEvent event) { Button quickDecisionButton = new Button("Quick Decision"); quickDecisionButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); actionLayout.add(quickDecisionButton); + + Button editSessionButton = new Button("Edit Session"); + editSessionButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + actionLayout.add(editSessionButton); + + Button deleteSessionButton = new Button("Delete Session"); + deleteSessionButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + deleteSessionButton.addClickListener(e -> { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Delete Session"); + confirmDialog.setText("Are you sure you want to delete this session?"); + confirmDialog.setCancelable(true); + confirmDialog.setConfirmText("Delete"); + confirmDialog.setConfirmButtonTheme("error primary"); + confirmDialog.addConfirmListener(ev -> { + raceWeekendService.deleteSession(sessionId); + getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, + new RouteParameters("weekendId", String.valueOf(weekendId)))); + }); + confirmDialog.open(); + }); + actionLayout.add(deleteSessionButton); } if (actionLayout.getComponentCount() > 0) { @@ -140,7 +176,51 @@ public void beforeEnter(BeforeEnterEvent event) { ))) ); - addAndExpand(grid); + add(grid); + + // Entrylist section + VerticalLayout entrylistLayout = new VerticalLayout(); + entrylistLayout.setPadding(true); + entrylistLayout.add(new H3("Entrylist")); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + Upload upload = new Upload(); + upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { + String json = new String(data, StandardCharsets.UTF_8); + try { + entrylistService.uploadEntrylistForSession(sessionId, weekendId, json); + getUI().ifPresent(ui -> ui.access(() -> { + Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + ui.getPage().reload(); + })); + } catch (IllegalArgumentException ex) { + getUI().ifPresent(ui -> ui.access(() -> + Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR) + )); + } + })); + upload.setAcceptedFileTypes("application/json", ".json"); + upload.setMaxFiles(1); + entrylistLayout.add(upload); + } + + StewardingEntrylist entrylist = entrylistService.getEntrylistBySessionId(sessionId); + if (entrylist != null) { + List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); + Grid entrylistGrid = new Grid<>(StewardingEntrylistEntry.class, false); + entrylistGrid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true).setFlexGrow(0).setSortable(true); + entrylistGrid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setSortable(true); + entrylistGrid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setSortable(true); + entrylistGrid.setItems(entries); + entrylistGrid.setSelectionMode(Grid.SelectionMode.NONE); + entrylistGrid.setColumnReorderingAllowed(true); + entrylistGrid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + entrylistLayout.add(entrylistGrid); + } + + addAndExpand(entrylistLayout); } private HorizontalLayout createDetailRow(String label, String value) { @@ -153,7 +233,7 @@ private HorizontalLayout createDetailRow(String label, String value) { return row; } - private void openReportIncidentDialog(Integer sessionId, Integer weekendId) { + private void openReportIncidentDialog(Integer sessionId, Integer weekendId, RaceWeekend weekend) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Report Incident"); dialog.setWidth("600px"); @@ -172,25 +252,35 @@ private void openReportIncidentDialog(Integer sessionId, Integer weekendId) { descriptionField.setWidthFull(); descriptionField.setMinHeight("100px"); - IntegerField lapField = new IntegerField("Lap"); - lapField.setMin(0); - lapField.setWidthFull(); + NumberField mapMarkerXField = new NumberField("Map Marker X"); + mapMarkerXField.setWidthFull(); - TextField timestampField = new TextField("Time in Session"); - timestampField.setWidthFull(); + NumberField mapMarkerYField = new NumberField("Map Marker Y"); + mapMarkerYField.setWidthFull(); - TextField involvedCarsField = new TextField("Involved Cars"); - involvedCarsField.setWidthFull(); - involvedCarsField.setPlaceholder("e.g. #001, #042"); - - TextField videoUrlField = new TextField("Video URL"); - videoUrlField.setWidthFull(); + // Populate involved cars from session entrylist + MultiSelectComboBox involvedCarsCombo = new MultiSelectComboBox<>("Involved Cars"); + involvedCarsCombo.setWidthFull(); + StewardingEntrylist entrylist = entrylistService.getEntrylistBySessionId(sessionId); + if (entrylist != null) { + List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); + involvedCarsCombo.setItems(entries); + } + involvedCarsCombo.setItemLabelGenerator(StewardingEntrylistEntry::getDisplayName); form.add(titleField, 2); form.add(descriptionField, 2); - form.add(lapField, timestampField); - form.add(involvedCarsField, 2); - form.add(videoUrlField, 2); + form.add(mapMarkerXField, mapMarkerYField); + form.add(involvedCarsCombo, 2); + + final TextField videoUrlField; + if (Boolean.TRUE.equals(weekend.getVideoUrlEnabled())) { + videoUrlField = new TextField("Video URL"); + videoUrlField.setWidthFull(); + form.add(videoUrlField, 2); + } else { + videoUrlField = null; + } Button saveButton = new Button("Report", e -> { if (titleField.isEmpty()) { @@ -199,18 +289,28 @@ private void openReportIncidentDialog(Integer sessionId, Integer weekendId) { return; } + Set selectedEntries = involvedCarsCombo.getValue(); + String involvedCarsText = selectedEntries.stream() + .map(StewardingEntrylistEntry::getDisplayName) + .collect(Collectors.joining(", ")); + List involvedEntryIds = selectedEntries.stream() + .map(StewardingEntrylistEntry::getId) + .collect(Collectors.toList()); + + String videoUrl = videoUrlField != null ? videoUrlField.getValue() : null; + Incident incident = Incident.builder() .sessionId(sessionId) .title(titleField.getValue()) .description(descriptionField.getValue()) - .lap(lapField.getValue()) - .timestampInSession(timestampField.getValue()) - .involvedCarsText(involvedCarsField.getValue()) - .videoUrl(videoUrlField.getValue()) + .mapMarkerX(mapMarkerXField.getValue()) + .mapMarkerY(mapMarkerYField.getValue()) + .involvedCarsText(involvedCarsText) + .videoUrl(videoUrl) .status(IncidentStatus.REPORTED) .build(); - incidentService.createIncident(incident, List.of()); + incidentService.createIncident(incident, involvedEntryIds); dialog.close(); Notification.show("Incident reported", 3000, Notification.Position.MIDDLE) .addThemeVariants(NotificationVariant.LUMO_SUCCESS); diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql new file mode 100644 index 00000000..ec2fa8e1 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql @@ -0,0 +1,9 @@ +-- Move entrylist from race weekend to session +ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES simdesk.stewarding_session (id); +UPDATE simdesk.stewarding_entrylist SET session_id = (SELECT id FROM simdesk.stewarding_session WHERE race_weekend_id = simdesk.stewarding_entrylist.race_weekend_id LIMIT 1); + +DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_race_weekend_id; +CREATE INDEX ix_stewarding_entrylist_session_id ON simdesk.stewarding_entrylist (session_id); + +-- Add video_url_enabled setting to race weekend +ALTER TABLE simdesk.stewarding_race_weekend ADD COLUMN video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql new file mode 100644 index 00000000..dcaaadb3 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql @@ -0,0 +1,9 @@ +-- Move entrylist from race weekend to session +ALTER TABLE stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES stewarding_session (id); +UPDATE stewarding_entrylist SET session_id = (SELECT id FROM stewarding_session WHERE race_weekend_id = stewarding_entrylist.race_weekend_id LIMIT 1); + +DROP INDEX IF EXISTS ix_stewarding_entrylist_race_weekend_id; +CREATE INDEX ix_stewarding_entrylist_session_id ON stewarding_entrylist (session_id); + +-- Add video_url_enabled setting to race weekend +ALTER TABLE stewarding_race_weekend ADD COLUMN video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE; From b63d9756ece076b644f6e0a3f14ae841ffab64f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:58:54 +0000 Subject: [PATCH 12/24] Fix migration to use deterministic ORDER BY for session assignment Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../postgres/V2_13_0__stewarding_entrylist_to_session.sql | 2 +- .../sqlite/V2_13_0__stewarding_entrylist_to_session.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql index ec2fa8e1..110c4f53 100644 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql @@ -1,6 +1,6 @@ -- Move entrylist from race weekend to session ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES simdesk.stewarding_session (id); -UPDATE simdesk.stewarding_entrylist SET session_id = (SELECT id FROM simdesk.stewarding_session WHERE race_weekend_id = simdesk.stewarding_entrylist.race_weekend_id LIMIT 1); +UPDATE simdesk.stewarding_entrylist SET session_id = (SELECT id FROM simdesk.stewarding_session WHERE race_weekend_id = simdesk.stewarding_entrylist.race_weekend_id ORDER BY sort_order, created_at LIMIT 1); DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_race_weekend_id; CREATE INDEX ix_stewarding_entrylist_session_id ON simdesk.stewarding_entrylist (session_id); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql index dcaaadb3..456aa35b 100644 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql @@ -1,6 +1,6 @@ -- Move entrylist from race weekend to session ALTER TABLE stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES stewarding_session (id); -UPDATE stewarding_entrylist SET session_id = (SELECT id FROM stewarding_session WHERE race_weekend_id = stewarding_entrylist.race_weekend_id LIMIT 1); +UPDATE stewarding_entrylist SET session_id = (SELECT id FROM stewarding_session WHERE race_weekend_id = stewarding_entrylist.race_weekend_id ORDER BY sort_order, created_at LIMIT 1); DROP INDEX IF EXISTS ix_stewarding_entrylist_race_weekend_id; CREATE INDEX ix_stewarding_entrylist_session_id ON stewarding_entrylist (session_id); From 836c062de120357fa7c7b3a56325e4be14131c28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:24:29 +0000 Subject: [PATCH 13/24] =?UTF-8?q?Restructure=20stewarding=20module:=20Seri?= =?UTF-8?q?es=20=E2=86=92=20Round=20=E2=86=92=20Session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create Flyway V2_14_0 migrations for SQLite and PostgreSQL - Add Series entity (new top-level, absorbs description/webhook/penalty from RaceWeekend) - Add Round entity (renamed from RaceWeekend, with series_id FK) - Add RoundSession entity (renamed from RaceWeekendSession, with roundId FK) - Update StewardingEntrylist to use roundId instead of raceWeekendId/sessionId - Delete old RaceWeekend and RaceWeekendSession entity files - Use source_race_weekend_id for deterministic migration mapping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../simdesk/entities/stewarding/Round.java | 29 +++++++ ...eWeekendSession.java => RoundSession.java} | 4 +- .../{RaceWeekend.java => Series.java} | 8 +- .../stewarding/StewardingEntrylist.java | 3 +- ...0__stewarding_series_round_restructure.sql | 77 +++++++++++++++++++ ...0__stewarding_series_round_restructure.sql | 71 +++++++++++++++++ 6 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java rename simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/{RaceWeekendSession.java => RoundSession.java} (88%) rename simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/{RaceWeekend.java => Series.java} (86%) create mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql create mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java new file mode 100644 index 00000000..94ad6fc5 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java @@ -0,0 +1,29 @@ +package de.sustineo.simdesk.entities.stewarding; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Round { + private Integer id; + private Integer seriesId; + private Integer trackId; + private String title; + private LocalDate startDate; + private LocalDate endDate; + private Instant createdAt; + private Instant updatedAt; + + @EqualsAndHashCode.Exclude + private StewardingTrack track; +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java similarity index 88% rename from simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java rename to simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java index 1f908bf5..556bc799 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekendSession.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java @@ -12,9 +12,9 @@ @Builder @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class RaceWeekendSession { +public class RoundSession { private Integer id; - private Integer raceWeekendId; + private Integer roundId; private StewSessionType sessionType; private String title; private Instant startTime; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java similarity index 86% rename from simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java rename to simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java index f2cf5448..d87f0a26 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RaceWeekend.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java @@ -14,22 +14,18 @@ @Builder @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class RaceWeekend { +public class Series { private Integer id; private String title; private String description; - private Integer trackId; - private Integer penaltyCatalogId; private String discordWebhookUrl; private Boolean videoUrlEnabled; + private Integer penaltyCatalogId; private LocalDate startDate; private LocalDate endDate; private Instant createdAt; private Instant updatedAt; - @EqualsAndHashCode.Exclude - private StewardingTrack track; - @EqualsAndHashCode.Exclude private PenaltyCatalog penaltyCatalog; } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java index 9cb0a4f0..fb4f6c2f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java @@ -14,8 +14,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardingEntrylist { private Integer id; - private Integer raceWeekendId; - private Integer sessionId; + private Integer roundId; private Instant uploadedAt; private String rawJson; } diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql new file mode 100644 index 00000000..2996f24e --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql @@ -0,0 +1,77 @@ +-- Restructure stewarding: Race Weekend -> Series + Round + Session + +-- 1. Create stewarding_series table with a temporary column for migration mapping +CREATE TABLE IF NOT EXISTS simdesk.stewarding_series +( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id INTEGER, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + source_race_weekend_id INTEGER UNIQUE, + FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) +); + +-- 2. Migrate existing race weekends into series (one series per race weekend) +-- Store the source race_weekend_id for deterministic mapping +INSERT INTO simdesk.stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at, source_race_weekend_id) +SELECT title || ' Series', + description, + discord_webhook_url, + video_url_enabled, + penalty_catalog_id, + start_date, + end_date, + created_at, + updated_at, + id +FROM simdesk.stewarding_race_weekend; + +-- 3. Add series_id column to stewarding_race_weekend (acts as round table) +ALTER TABLE simdesk.stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES simdesk.stewarding_series (id); + +-- 4. Update each race weekend to point to its corresponding series using deterministic mapping +UPDATE simdesk.stewarding_race_weekend rw +SET series_id = s.id +FROM simdesk.stewarding_series s +WHERE s.source_race_weekend_id = rw.id; + +CREATE INDEX ix_stewarding_race_weekend_series_id ON simdesk.stewarding_race_weekend (series_id); + +-- 5. Add round_id column to stewarding_entrylist and populate from session's race_weekend_id +ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES simdesk.stewarding_race_weekend (id); + +-- Populate round_id from session_id -> stewarding_session.race_weekend_id +UPDATE simdesk.stewarding_entrylist el +SET round_id = ss.race_weekend_id +FROM simdesk.stewarding_session ss +WHERE ss.id = el.session_id + AND el.session_id IS NOT NULL; + +-- Fall back to race_weekend_id for entries that have no session_id +UPDATE simdesk.stewarding_entrylist +SET round_id = race_weekend_id +WHERE round_id IS NULL AND race_weekend_id IS NOT NULL; + +CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); + +-- 6. Drop old columns from stewarding_race_weekend that moved to series +ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS description; +ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS discord_webhook_url; +ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS video_url_enabled; +ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS penalty_catalog_id; +DROP INDEX IF EXISTS simdesk.ix_stewarding_race_weekend_penalty_catalog_id; + +-- 7. Drop old columns from stewarding_entrylist +ALTER TABLE simdesk.stewarding_entrylist DROP COLUMN IF EXISTS race_weekend_id; +ALTER TABLE simdesk.stewarding_entrylist DROP COLUMN IF EXISTS session_id; +DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_race_weekend_id; +DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_session_id; + +-- 8. Drop temporary migration mapping column from stewarding_series +ALTER TABLE simdesk.stewarding_series DROP COLUMN IF EXISTS source_race_weekend_id; diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql new file mode 100644 index 00000000..ab6997ad --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql @@ -0,0 +1,71 @@ +-- Restructure stewarding: Race Weekend -> Series + Round + Session + +-- 1. Create stewarding_series table with a temporary column for migration mapping +CREATE TABLE IF NOT EXISTS stewarding_series +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id INTEGER, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + source_race_weekend_id INTEGER UNIQUE, + FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) +); + +-- 2. Migrate existing race weekends into series (one series per race weekend) +-- Store the source race_weekend_id for deterministic mapping +INSERT INTO stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at, source_race_weekend_id) +SELECT title || ' Series', + description, + discord_webhook_url, + video_url_enabled, + penalty_catalog_id, + start_date, + end_date, + created_at, + updated_at, + id +FROM stewarding_race_weekend; + +-- 3. Add series_id column to stewarding_race_weekend (acts as round table) +ALTER TABLE stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES stewarding_series (id); + +-- 4. Update each race weekend to point to its corresponding series using deterministic mapping +UPDATE stewarding_race_weekend +SET series_id = ( + SELECT s.id + FROM stewarding_series s + WHERE s.source_race_weekend_id = stewarding_race_weekend.id +); + +CREATE INDEX ix_stewarding_race_weekend_series_id ON stewarding_race_weekend (series_id); + +-- 5. Add round_id column to stewarding_entrylist and populate from session's race_weekend_id +ALTER TABLE stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES stewarding_race_weekend (id); + +-- Populate round_id from session_id -> stewarding_session.race_weekend_id +UPDATE stewarding_entrylist +SET round_id = ( + SELECT ss.race_weekend_id + FROM stewarding_session ss + WHERE ss.id = stewarding_entrylist.session_id +) +WHERE session_id IS NOT NULL; + +-- Fall back to race_weekend_id for entries that have no session_id +UPDATE stewarding_entrylist +SET round_id = race_weekend_id +WHERE round_id IS NULL AND race_weekend_id IS NOT NULL; + +CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); + +-- Note: SQLite does not support DROP COLUMN easily. +-- Old columns (description, discord_webhook_url, video_url_enabled, penalty_catalog_id) +-- remain on stewarding_race_weekend but are no longer used by the application. +-- Old columns (race_weekend_id, session_id) remain on stewarding_entrylist but are no longer used. +-- The source_race_weekend_id column on stewarding_series was used for migration mapping and is no longer used. From 0ed2282bfb6529def3b8fe421c0b6799f21a4772 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:29:34 +0000 Subject: [PATCH 14/24] Add Series/Round/RoundSession mappers and update StewardingEntrylistMapper - Create SeriesMapper for stewarding_series table - Create RoundMapper for stewarding_race_weekend table (replacing RaceWeekendMapper) - Create RoundSessionMapper for stewarding_session table (replacing RaceWeekendSessionMapper) - Update StewardingEntrylistMapper to use round_id instead of race_weekend_id/session_id - Delete old RaceWeekendMapper and RaceWeekendSessionMapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../mybatis/mapper/RaceWeekendMapper.java | 53 ------------------ .../simdesk/mybatis/mapper/RoundMapper.java | 54 +++++++++++++++++++ ...ionMapper.java => RoundSessionMapper.java} | 24 ++++----- .../simdesk/mybatis/mapper/SeriesMapper.java | 49 +++++++++++++++++ .../mapper/StewardingEntrylistMapper.java | 22 +++----- 5 files changed, 122 insertions(+), 80 deletions(-) delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java rename simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/{RaceWeekendSessionMapper.java => RoundSessionMapper.java} (62%) create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java deleted file mode 100644 index dc313b18..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendMapper.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.sustineo.simdesk.mybatis.mapper; - -import de.sustineo.simdesk.entities.stewarding.RaceWeekend; -import org.apache.ibatis.annotations.*; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@Mapper -public interface RaceWeekendMapper { - @Results(id = "raceWeekendResultMap", value = { - @Result(id = true, property = "id", column = "id"), - @Result(property = "title", column = "title"), - @Result(property = "description", column = "description"), - @Result(property = "trackId", column = "track_id"), - @Result(property = "penaltyCatalogId", column = "penalty_catalog_id"), - @Result(property = "discordWebhookUrl", column = "discord_webhook_url"), - @Result(property = "videoUrlEnabled", column = "video_url_enabled"), - @Result(property = "startDate", column = "start_date"), - @Result(property = "endDate", column = "end_date"), - @Result(property = "createdAt", column = "created_at"), - @Result(property = "updatedAt", column = "updated_at"), - }) - @Select("SELECT * FROM stewarding_race_weekend ORDER BY start_date DESC") - List findAll(); - - @ResultMap("raceWeekendResultMap") - @Select("SELECT * FROM stewarding_race_weekend WHERE id = #{id}") - RaceWeekend findById(Integer id); - - @ResultMap("raceWeekendResultMap") - @Select("SELECT * FROM stewarding_race_weekend WHERE track_id = #{trackId}") - List findByTrackId(Integer trackId); - - @Insert(""" - INSERT INTO stewarding_race_weekend (title, description, track_id, penalty_catalog_id, discord_webhook_url, video_url_enabled, start_date, end_date, created_at, updated_at) - VALUES (#{title}, #{description}, #{trackId}, #{penaltyCatalogId}, #{discordWebhookUrl}, #{videoUrlEnabled}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") - void insert(RaceWeekend weekend); - - @Update(""" - UPDATE stewarding_race_weekend - SET title = #{title}, description = #{description}, track_id = #{trackId}, penalty_catalog_id = #{penaltyCatalogId}, - discord_webhook_url = #{discordWebhookUrl}, video_url_enabled = #{videoUrlEnabled}, start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP - WHERE id = #{id} - """) - void update(RaceWeekend weekend); - - @Delete("DELETE FROM stewarding_race_weekend WHERE id = #{id}") - void delete(Integer id); -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java new file mode 100644 index 00000000..99fee5c1 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java @@ -0,0 +1,54 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.Round; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface RoundMapper { + @Results(id = "roundResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "seriesId", column = "series_id"), + @Result(property = "trackId", column = "track_id"), + @Result(property = "title", column = "title"), + @Result(property = "startDate", column = "start_date"), + @Result(property = "endDate", column = "end_date"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_race_weekend ORDER BY start_date DESC") + List findAll(); + + @ResultMap("roundResultMap") + @Select("SELECT * FROM stewarding_race_weekend WHERE id = #{id}") + Round findById(Integer id); + + @ResultMap("roundResultMap") + @Select("SELECT * FROM stewarding_race_weekend WHERE series_id = #{seriesId} ORDER BY start_date") + List findBySeriesId(Integer seriesId); + + @ResultMap("roundResultMap") + @Select("SELECT * FROM stewarding_race_weekend WHERE track_id = #{trackId}") + List findByTrackId(Integer trackId); + + @Insert(""" + INSERT INTO stewarding_race_weekend (series_id, track_id, title, start_date, end_date, created_at, updated_at) + VALUES (#{seriesId}, #{trackId}, #{title}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(Round round); + + @Update(""" + UPDATE stewarding_race_weekend + SET series_id = #{seriesId}, track_id = #{trackId}, title = #{title}, + start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(Round round); + + @Delete("DELETE FROM stewarding_race_weekend WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java similarity index 62% rename from simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java rename to simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java index 1d6264db..f0cdf582 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RaceWeekendSessionMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java @@ -1,6 +1,6 @@ package de.sustineo.simdesk.mybatis.mapper; -import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; +import de.sustineo.simdesk.entities.stewarding.RoundSession; import org.apache.ibatis.annotations.*; import org.springframework.stereotype.Component; @@ -8,10 +8,10 @@ @Component @Mapper -public interface RaceWeekendSessionMapper { - @Results(id = "raceWeekendSessionResultMap", value = { +public interface RoundSessionMapper { + @Results(id = "roundSessionResultMap", value = { @Result(id = true, property = "id", column = "id"), - @Result(property = "raceWeekendId", column = "race_weekend_id"), + @Result(property = "roundId", column = "race_weekend_id"), @Result(property = "sessionType", column = "session_type"), @Result(property = "title", column = "title"), @Result(property = "startTime", column = "start_time"), @@ -19,27 +19,27 @@ public interface RaceWeekendSessionMapper { @Result(property = "sortOrder", column = "sort_order"), @Result(property = "createdAt", column = "created_at"), }) - @Select("SELECT * FROM stewarding_session WHERE race_weekend_id = #{raceWeekendId} ORDER BY sort_order") - List findByRaceWeekendId(Integer raceWeekendId); + @Select("SELECT * FROM stewarding_session WHERE race_weekend_id = #{roundId} ORDER BY sort_order") + List findByRoundId(Integer roundId); - @ResultMap("raceWeekendSessionResultMap") + @ResultMap("roundSessionResultMap") @Select("SELECT * FROM stewarding_session WHERE id = #{id}") - RaceWeekendSession findById(Integer id); + RoundSession findById(Integer id); @Insert(""" INSERT INTO stewarding_session (race_weekend_id, session_type, title, start_time, end_time, sort_order, created_at) - VALUES (#{raceWeekendId}, #{sessionType}, #{title}, #{startTime}, #{endTime}, #{sortOrder}, CURRENT_TIMESTAMP) + VALUES (#{roundId}, #{sessionType}, #{title}, #{startTime}, #{endTime}, #{sortOrder}, CURRENT_TIMESTAMP) """) @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") - void insert(RaceWeekendSession session); + void insert(RoundSession session); @Update(""" UPDATE stewarding_session - SET race_weekend_id = #{raceWeekendId}, session_type = #{sessionType}, title = #{title}, + SET race_weekend_id = #{roundId}, session_type = #{sessionType}, title = #{title}, start_time = #{startTime}, end_time = #{endTime}, sort_order = #{sortOrder} WHERE id = #{id} """) - void update(RaceWeekendSession session); + void update(RoundSession session); @Delete("DELETE FROM stewarding_session WHERE id = #{id}") void delete(Integer id); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java new file mode 100644 index 00000000..8a0e8dc8 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java @@ -0,0 +1,49 @@ +package de.sustineo.simdesk.mybatis.mapper; + +import de.sustineo.simdesk.entities.stewarding.Series; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface SeriesMapper { + @Results(id = "seriesResultMap", value = { + @Result(id = true, property = "id", column = "id"), + @Result(property = "title", column = "title"), + @Result(property = "description", column = "description"), + @Result(property = "discordWebhookUrl", column = "discord_webhook_url"), + @Result(property = "videoUrlEnabled", column = "video_url_enabled"), + @Result(property = "penaltyCatalogId", column = "penalty_catalog_id"), + @Result(property = "startDate", column = "start_date"), + @Result(property = "endDate", column = "end_date"), + @Result(property = "createdAt", column = "created_at"), + @Result(property = "updatedAt", column = "updated_at"), + }) + @Select("SELECT * FROM stewarding_series ORDER BY start_date DESC") + List findAll(); + + @ResultMap("seriesResultMap") + @Select("SELECT * FROM stewarding_series WHERE id = #{id}") + Series findById(Integer id); + + @Insert(""" + INSERT INTO stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at) + VALUES (#{title}, #{description}, #{discordWebhookUrl}, #{videoUrlEnabled}, #{penaltyCatalogId}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void insert(Series series); + + @Update(""" + UPDATE stewarding_series + SET title = #{title}, description = #{description}, discord_webhook_url = #{discordWebhookUrl}, + video_url_enabled = #{videoUrlEnabled}, penalty_catalog_id = #{penaltyCatalogId}, + start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP + WHERE id = #{id} + """) + void update(Series series); + + @Delete("DELETE FROM stewarding_series WHERE id = #{id}") + void delete(Integer id); +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java index 6041bea6..5777df8c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java @@ -11,32 +11,24 @@ public interface StewardingEntrylistMapper { @Results(id = "stewardingEntrylistResultMap", value = { @Result(id = true, property = "id", column = "id"), - @Result(property = "raceWeekendId", column = "race_weekend_id"), - @Result(property = "sessionId", column = "session_id"), + @Result(property = "roundId", column = "round_id"), @Result(property = "uploadedAt", column = "uploaded_at"), @Result(property = "rawJson", column = "raw_json"), }) - @Select("SELECT * FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") - List findByRaceWeekendId(Integer raceWeekendId); - - @ResultMap("stewardingEntrylistResultMap") - @Select("SELECT * FROM stewarding_entrylist WHERE session_id = #{sessionId}") - List findBySessionId(Integer sessionId); + @Select("SELECT * FROM stewarding_entrylist WHERE round_id = #{roundId}") + List findByRoundId(Integer roundId); @ResultMap("stewardingEntrylistResultMap") @Select("SELECT * FROM stewarding_entrylist WHERE id = #{id}") StewardingEntrylist findById(Integer id); @Insert(""" - INSERT INTO stewarding_entrylist (race_weekend_id, session_id, uploaded_at, raw_json) - VALUES (#{raceWeekendId}, #{sessionId}, CURRENT_TIMESTAMP, #{rawJson}) + INSERT INTO stewarding_entrylist (round_id, uploaded_at, raw_json) + VALUES (#{roundId}, CURRENT_TIMESTAMP, #{rawJson}) """) @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingEntrylist entrylist); - @Delete("DELETE FROM stewarding_entrylist WHERE race_weekend_id = #{raceWeekendId}") - void deleteByRaceWeekendId(Integer raceWeekendId); - - @Delete("DELETE FROM stewarding_entrylist WHERE session_id = #{sessionId}") - void deleteBySessionId(Integer sessionId); + @Delete("DELETE FROM stewarding_entrylist WHERE round_id = #{roundId}") + void deleteByRoundId(Integer roundId); } From c8c7691c7f87e814484b25eb4cd5fe778249750a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:35:13 +0000 Subject: [PATCH 15/24] refactor: replace RaceWeekend with Series/Round/RoundSession service layer - Create SeriesService for Series CRUD with PenaltyCatalog enrichment - Create RoundService for Round + RoundSession CRUD with Track enrichment - Delete RaceWeekendService (replaced by SeriesService + RoundService) - Update StewardingEntrylistService to use round-level operations - Update StewardingDiscordNotificationService to get webhook URL from Series Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../stewarding/RaceWeekendService.java | 80 ------------------- .../services/stewarding/RoundService.java | 77 ++++++++++++++++++ .../services/stewarding/SeriesService.java | 47 +++++++++++ .../StewardingDiscordNotificationService.java | 36 ++++----- .../StewardingEntrylistService.java | 64 +++------------ ...0__stewarding_series_round_restructure.sql | 78 ++++-------------- ...0__stewarding_series_round_restructure.sql | 72 ++++------------- 7 files changed, 183 insertions(+), 271 deletions(-) delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java deleted file mode 100644 index 4908f04f..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RaceWeekendService.java +++ /dev/null @@ -1,80 +0,0 @@ -package de.sustineo.simdesk.services.stewarding; - -import de.sustineo.simdesk.configuration.SpringProfile; -import de.sustineo.simdesk.entities.stewarding.RaceWeekend; -import de.sustineo.simdesk.entities.stewarding.RaceWeekendSession; -import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; -import de.sustineo.simdesk.mybatis.mapper.RaceWeekendMapper; -import de.sustineo.simdesk.mybatis.mapper.RaceWeekendSessionMapper; -import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Profile(SpringProfile.STEWARDING) -@Service -@RequiredArgsConstructor -public class RaceWeekendService { - private final RaceWeekendMapper weekendMapper; - private final RaceWeekendSessionMapper sessionMapper; - private final StewardingTrackMapper trackMapper; - private final PenaltyCatalogMapper catalogMapper; - - public List getAllWeekends() { - return weekendMapper.findAll(); - } - - public RaceWeekend getWeekendById(Integer id) { - RaceWeekend weekend = weekendMapper.findById(id); - if (weekend != null) { - if (weekend.getTrackId() != null) { - weekend.setTrack(trackMapper.findById(weekend.getTrackId())); - } - if (weekend.getPenaltyCatalogId() != null) { - weekend.setPenaltyCatalog(catalogMapper.findById(weekend.getPenaltyCatalogId())); - } - } - return weekend; - } - - @Transactional - public void createWeekend(RaceWeekend weekend) { - weekendMapper.insert(weekend); - } - - @Transactional - public void updateWeekend(RaceWeekend weekend) { - weekendMapper.update(weekend); - } - - @Transactional - public void deleteWeekend(Integer id) { - weekendMapper.delete(id); - } - - public List getSessionsByWeekendId(Integer weekendId) { - return sessionMapper.findByRaceWeekendId(weekendId); - } - - public RaceWeekendSession getSessionById(Integer id) { - return sessionMapper.findById(id); - } - - @Transactional - public void createSession(RaceWeekendSession session) { - sessionMapper.insert(session); - } - - @Transactional - public void updateSession(RaceWeekendSession session) { - sessionMapper.update(session); - } - - @Transactional - public void deleteSession(Integer id) { - sessionMapper.delete(id); - } -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java new file mode 100644 index 00000000..e6e475e0 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java @@ -0,0 +1,77 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.Round; +import de.sustineo.simdesk.entities.stewarding.RoundSession; +import de.sustineo.simdesk.mybatis.mapper.RoundMapper; +import de.sustineo.simdesk.mybatis.mapper.RoundSessionMapper; +import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class RoundService { + private final RoundMapper roundMapper; + private final RoundSessionMapper sessionMapper; + private final StewardingTrackMapper trackMapper; + + public List getAllRounds() { + return roundMapper.findAll(); + } + + public Round getRoundById(Integer id) { + Round round = roundMapper.findById(id); + if (round != null && round.getTrackId() != null) { + round.setTrack(trackMapper.findById(round.getTrackId())); + } + return round; + } + + public List getRoundsBySeriesId(Integer seriesId) { + return roundMapper.findBySeriesId(seriesId); + } + + @Transactional + public void createRound(Round round) { + roundMapper.insert(round); + } + + @Transactional + public void updateRound(Round round) { + roundMapper.update(round); + } + + @Transactional + public void deleteRound(Integer id) { + roundMapper.delete(id); + } + + public List getSessionsByRoundId(Integer roundId) { + return sessionMapper.findByRoundId(roundId); + } + + public RoundSession getSessionById(Integer id) { + return sessionMapper.findById(id); + } + + @Transactional + public void createSession(RoundSession session) { + sessionMapper.insert(session); + } + + @Transactional + public void updateSession(RoundSession session) { + sessionMapper.update(session); + } + + @Transactional + public void deleteSession(Integer id) { + sessionMapper.delete(id); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java new file mode 100644 index 00000000..bee09847 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java @@ -0,0 +1,47 @@ +package de.sustineo.simdesk.services.stewarding; + +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.stewarding.Series; +import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; +import de.sustineo.simdesk.mybatis.mapper.SeriesMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Service +@RequiredArgsConstructor +public class SeriesService { + private final SeriesMapper seriesMapper; + private final PenaltyCatalogMapper catalogMapper; + + public List getAllSeries() { + return seriesMapper.findAll(); + } + + public Series getSeriesById(Integer id) { + Series series = seriesMapper.findById(id); + if (series != null && series.getPenaltyCatalogId() != null) { + series.setPenaltyCatalog(catalogMapper.findById(series.getPenaltyCatalogId())); + } + return series; + } + + @Transactional + public void createSeries(Series series) { + seriesMapper.insert(series); + } + + @Transactional + public void updateSeries(Series series) { + seriesMapper.update(series); + } + + @Transactional + public void deleteSeries(Integer id) { + seriesMapper.delete(id); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java index 08f94548..e9d10266 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java @@ -2,7 +2,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.*; -import de.sustineo.simdesk.mybatis.mapper.RaceWeekendMapper; +import de.sustineo.simdesk.mybatis.mapper.SeriesMapper; import lombok.extern.java.Log; import org.springframework.context.annotation.Profile; import org.springframework.http.MediaType; @@ -25,17 +25,17 @@ public class StewardingDiscordNotificationService { private static final int COLOR_YELLOW = 0xF1C40F; private static final int COLOR_ORANGE = 0xE67E22; - private final RaceWeekendMapper weekendMapper; + private final SeriesMapper seriesMapper; private final RestClient restClient; - public StewardingDiscordNotificationService(RaceWeekendMapper weekendMapper) { - this.weekendMapper = weekendMapper; + public StewardingDiscordNotificationService(SeriesMapper seriesMapper) { + this.seriesMapper = seriesMapper; this.restClient = RestClient.create(); } @Async - public void sendIncidentNotification(Integer raceWeekendId, Incident incident) { - String webhookUrl = getWebhookUrl(raceWeekendId); + public void sendIncidentNotification(Integer seriesId, Incident incident) { + String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; } @@ -55,8 +55,8 @@ public void sendIncidentNotification(Integer raceWeekendId, Incident incident) { } @Async - public void sendDecisionNotification(Integer raceWeekendId, StewardDecision decision, Incident incident, String penaltyName) { - String webhookUrl = getWebhookUrl(raceWeekendId); + public void sendDecisionNotification(Integer seriesId, StewardDecision decision, Incident incident, String penaltyName) { + String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; } @@ -80,8 +80,8 @@ public void sendDecisionNotification(Integer raceWeekendId, StewardDecision deci } @Async - public void sendAppealNotification(Integer raceWeekendId, Appeal appeal) { - String webhookUrl = getWebhookUrl(raceWeekendId); + public void sendAppealNotification(Integer seriesId, Appeal appeal) { + String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; } @@ -101,8 +101,8 @@ public void sendAppealNotification(Integer raceWeekendId, Appeal appeal) { } @Async - public void sendAppealReviewedNotification(Integer raceWeekendId, Appeal appeal) { - String webhookUrl = getWebhookUrl(raceWeekendId); + public void sendAppealReviewedNotification(Integer seriesId, Appeal appeal) { + String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; } @@ -124,8 +124,8 @@ public void sendAppealReviewedNotification(Integer raceWeekendId, Appeal appeal) } @Async - public void sendDecisionRevisedNotification(Integer raceWeekendId, StewardDecision oldDecision, StewardDecision newDecision) { - String webhookUrl = getWebhookUrl(raceWeekendId); + public void sendDecisionRevisedNotification(Integer seriesId, StewardDecision oldDecision, StewardDecision newDecision) { + String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; } @@ -145,12 +145,12 @@ public void sendDecisionRevisedNotification(Integer raceWeekendId, StewardDecisi sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); } - private String getWebhookUrl(Integer raceWeekendId) { - RaceWeekend weekend = weekendMapper.findById(raceWeekendId); - if (weekend == null || weekend.getDiscordWebhookUrl() == null || weekend.getDiscordWebhookUrl().isBlank()) { + private String getWebhookUrl(Integer seriesId) { + Series series = seriesMapper.findById(seriesId); + if (series == null || series.getDiscordWebhookUrl() == null || series.getDiscordWebhookUrl().isBlank()) { return null; } - return weekend.getDiscordWebhookUrl(); + return series.getDiscordWebhookUrl(); } private void sendWebhook(String webhookUrl, Map payload) { diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java index 65b43eda..d198e02f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java @@ -25,13 +25,8 @@ public class StewardingEntrylistService { private final StewardingEntrylistDriverMapper driverMapper; private final ObjectMapper objectMapper; - public StewardingEntrylist getEntrylistByWeekendId(Integer weekendId) { - List entrylists = entrylistMapper.findByRaceWeekendId(weekendId); - return entrylists.isEmpty() ? null : entrylists.getFirst(); - } - - public StewardingEntrylist getEntrylistBySessionId(Integer sessionId) { - List entrylists = entrylistMapper.findBySessionId(sessionId); + public StewardingEntrylist getEntrylistByRoundId(Integer roundId) { + List entrylists = entrylistMapper.findByRoundId(roundId); return entrylists.isEmpty() ? null : entrylists.getFirst(); } @@ -44,27 +39,8 @@ public List getDriversByEntryId(Integer entryId) { } @Transactional - public void uploadEntrylist(Integer weekendId, String jsonContent) { - deleteEntrylist(weekendId); - - JsonNode root; - try { - root = objectMapper.readTree(jsonContent); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid JSON content", e); - } - - StewardingEntrylist entrylist = new StewardingEntrylist(); - entrylist.setRaceWeekendId(weekendId); - entrylist.setRawJson(jsonContent); - entrylistMapper.insert(entrylist); - - parseAndInsertEntries(root, entrylist); - } - - @Transactional - public void uploadEntrylistForSession(Integer sessionId, Integer weekendId, String jsonContent) { - deleteEntrylistForSession(sessionId); + public void uploadEntrylistForRound(Integer roundId, String jsonContent) { + deleteEntrylistForRound(roundId); JsonNode root; try { @@ -74,8 +50,7 @@ public void uploadEntrylistForSession(Integer sessionId, Integer weekendId, Stri } StewardingEntrylist entrylist = new StewardingEntrylist(); - entrylist.setSessionId(sessionId); - entrylist.setRaceWeekendId(weekendId); + entrylist.setRoundId(roundId); entrylist.setRawJson(jsonContent); entrylistMapper.insert(entrylist); @@ -123,28 +98,15 @@ private void parseAndInsertEntries(JsonNode root, StewardingEntrylist entrylist) } @Transactional - public void deleteEntrylist(Integer weekendId) { - List existing = entrylistMapper.findByRaceWeekendId(weekendId); - for (StewardingEntrylist entrylist : existing) { - deleteEntrylistEntries(entrylist); - } - entrylistMapper.deleteByRaceWeekendId(weekendId); - } - - @Transactional - public void deleteEntrylistForSession(Integer sessionId) { - List existing = entrylistMapper.findBySessionId(sessionId); + public void deleteEntrylistForRound(Integer roundId) { + List existing = entrylistMapper.findByRoundId(roundId); for (StewardingEntrylist entrylist : existing) { - deleteEntrylistEntries(entrylist); - } - entrylistMapper.deleteBySessionId(sessionId); - } - - private void deleteEntrylistEntries(StewardingEntrylist entrylist) { - List entries = entryMapper.findByEntrylistId(entrylist.getId()); - for (StewardingEntrylistEntry entry : entries) { - driverMapper.deleteByEntryId(entry.getId()); + List entries = entryMapper.findByEntrylistId(entrylist.getId()); + for (StewardingEntrylistEntry entry : entries) { + driverMapper.deleteByEntryId(entry.getId()); + } + entryMapper.deleteByEntrylistId(entrylist.getId()); } - entryMapper.deleteByEntrylistId(entrylist.getId()); + entrylistMapper.deleteByRoundId(roundId); } } diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql index 2996f24e..c570a1f1 100644 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql @@ -1,77 +1,27 @@ --- Restructure stewarding: Race Weekend -> Series + Round + Session +-- Restructure stewarding: Race Weekend -> Series + Round + Session (schema only) --- 1. Create stewarding_series table with a temporary column for migration mapping +-- 1. Create stewarding_series table CREATE TABLE IF NOT EXISTS simdesk.stewarding_series ( - id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id INTEGER, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - source_race_weekend_id INTEGER UNIQUE, + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id INTEGER, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) ); --- 2. Migrate existing race weekends into series (one series per race weekend) --- Store the source race_weekend_id for deterministic mapping -INSERT INTO simdesk.stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at, source_race_weekend_id) -SELECT title || ' Series', - description, - discord_webhook_url, - video_url_enabled, - penalty_catalog_id, - start_date, - end_date, - created_at, - updated_at, - id -FROM simdesk.stewarding_race_weekend; - --- 3. Add series_id column to stewarding_race_weekend (acts as round table) +-- 2. Add series_id column to stewarding_race_weekend (acts as round table) ALTER TABLE simdesk.stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES simdesk.stewarding_series (id); --- 4. Update each race weekend to point to its corresponding series using deterministic mapping -UPDATE simdesk.stewarding_race_weekend rw -SET series_id = s.id -FROM simdesk.stewarding_series s -WHERE s.source_race_weekend_id = rw.id; - CREATE INDEX ix_stewarding_race_weekend_series_id ON simdesk.stewarding_race_weekend (series_id); --- 5. Add round_id column to stewarding_entrylist and populate from session's race_weekend_id +-- 3. Add round_id column to stewarding_entrylist ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES simdesk.stewarding_race_weekend (id); --- Populate round_id from session_id -> stewarding_session.race_weekend_id -UPDATE simdesk.stewarding_entrylist el -SET round_id = ss.race_weekend_id -FROM simdesk.stewarding_session ss -WHERE ss.id = el.session_id - AND el.session_id IS NOT NULL; - --- Fall back to race_weekend_id for entries that have no session_id -UPDATE simdesk.stewarding_entrylist -SET round_id = race_weekend_id -WHERE round_id IS NULL AND race_weekend_id IS NOT NULL; - CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); - --- 6. Drop old columns from stewarding_race_weekend that moved to series -ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS description; -ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS discord_webhook_url; -ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS video_url_enabled; -ALTER TABLE simdesk.stewarding_race_weekend DROP COLUMN IF EXISTS penalty_catalog_id; -DROP INDEX IF EXISTS simdesk.ix_stewarding_race_weekend_penalty_catalog_id; - --- 7. Drop old columns from stewarding_entrylist -ALTER TABLE simdesk.stewarding_entrylist DROP COLUMN IF EXISTS race_weekend_id; -ALTER TABLE simdesk.stewarding_entrylist DROP COLUMN IF EXISTS session_id; -DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_race_weekend_id; -DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_session_id; - --- 8. Drop temporary migration mapping column from stewarding_series -ALTER TABLE simdesk.stewarding_series DROP COLUMN IF EXISTS source_race_weekend_id; diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql index ab6997ad..c73cad4d 100644 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql @@ -1,71 +1,27 @@ --- Restructure stewarding: Race Weekend -> Series + Round + Session +-- Restructure stewarding: Race Weekend -> Series + Round + Session (schema only) --- 1. Create stewarding_series table with a temporary column for migration mapping +-- 1. Create stewarding_series table CREATE TABLE IF NOT EXISTS stewarding_series ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id INTEGER, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - source_race_weekend_id INTEGER UNIQUE, + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id INTEGER, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) ); --- 2. Migrate existing race weekends into series (one series per race weekend) --- Store the source race_weekend_id for deterministic mapping -INSERT INTO stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at, source_race_weekend_id) -SELECT title || ' Series', - description, - discord_webhook_url, - video_url_enabled, - penalty_catalog_id, - start_date, - end_date, - created_at, - updated_at, - id -FROM stewarding_race_weekend; - --- 3. Add series_id column to stewarding_race_weekend (acts as round table) +-- 2. Add series_id column to stewarding_race_weekend (acts as round table) ALTER TABLE stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES stewarding_series (id); --- 4. Update each race weekend to point to its corresponding series using deterministic mapping -UPDATE stewarding_race_weekend -SET series_id = ( - SELECT s.id - FROM stewarding_series s - WHERE s.source_race_weekend_id = stewarding_race_weekend.id -); - CREATE INDEX ix_stewarding_race_weekend_series_id ON stewarding_race_weekend (series_id); --- 5. Add round_id column to stewarding_entrylist and populate from session's race_weekend_id +-- 3. Add round_id column to stewarding_entrylist ALTER TABLE stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES stewarding_race_weekend (id); --- Populate round_id from session_id -> stewarding_session.race_weekend_id -UPDATE stewarding_entrylist -SET round_id = ( - SELECT ss.race_weekend_id - FROM stewarding_session ss - WHERE ss.id = stewarding_entrylist.session_id -) -WHERE session_id IS NOT NULL; - --- Fall back to race_weekend_id for entries that have no session_id -UPDATE stewarding_entrylist -SET round_id = race_weekend_id -WHERE round_id IS NULL AND race_weekend_id IS NOT NULL; - CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); - --- Note: SQLite does not support DROP COLUMN easily. --- Old columns (description, discord_webhook_url, video_url_enabled, penalty_catalog_id) --- remain on stewarding_race_weekend but are no longer used by the application. --- Old columns (race_weekend_id, session_id) remain on stewarding_entrylist but are no longer used. --- The source_race_weekend_id column on stewarding_series was used for migration mapping and is no longer used. From fb3a531ae23145659b8d57f41a84cfed27d2527f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:41:06 +0000 Subject: [PATCH 16/24] =?UTF-8?q?Refactor=20stewarding=20views=20to=20Seri?= =?UTF-8?q?es=20=E2=86=92=20Round=20=E2=86=92=20Session=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create SeriesListView at /stewarding/series - Create SeriesDetailView at /stewarding/series/:seriesId - Create RoundDetailView at /stewarding/series/:seriesId/rounds/:roundId - Update IncidentDetailView route to /stewarding/series/:seriesId/rounds/:roundId/incidents/:incidentId - Delete RaceWeekendListView, RaceWeekendDetailView, RaceWeekendSessionDetailView - Update MenuService to reference SeriesListView Sessions are now listed within the Round detail (not navigable as separate pages). Entrylist and incident reporting are scoped to the Round level. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../simdesk/services/MenuService.java | 2 +- .../views/stewarding/IncidentDetailView.java | 60 +- .../stewarding/RaceWeekendDetailView.java | 388 ------------- .../RaceWeekendSessionDetailView.java | 327 ----------- .../views/stewarding/RoundDetailView.java | 527 ++++++++++++++++++ .../views/stewarding/SeriesDetailView.java | 330 +++++++++++ ...ekendListView.java => SeriesListView.java} | 66 +-- 7 files changed, 917 insertions(+), 783 deletions(-) delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java delete mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java create mode 100644 simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java rename simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/{RaceWeekendListView.java => SeriesListView.java} (67%) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java index 9cfc2ddf..2e60ff46 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/MenuService.java @@ -57,7 +57,7 @@ public List getItems() { } if (SpringProfile.isStewardingEnabled()) { - items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Race Weekends", VaadinIcon.CALENDAR, RaceWeekendListView.class)); + items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Series", VaadinIcon.CALENDAR, SeriesListView.class)); if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { items.add(MenuEntity.ofInternal(MenuEntityCategory.STEWARDING, "Tracks", VaadinIcon.ROAD, StewardingTrackListView.class)); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index 05023631..cd6c9ea7 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -33,7 +33,7 @@ import java.util.List; @Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/weekends/:weekendId/incidents/:incidentId", layout = MainLayout.class) +@Route(value = "/stewarding/series/:seriesId/rounds/:roundId/incidents/:incidentId", layout = MainLayout.class) @AnonymousAllowed public class IncidentDetailView extends BaseView { private final StewardingIncidentService incidentService; @@ -41,20 +41,23 @@ public class IncidentDetailView extends BaseView { private final StewardingAppealService appealService; private final PenaltyCatalogService catalogService; private final ReasoningTemplateService templateService; - private final RaceWeekendService raceWeekendService; + private final SeriesService seriesService; + private final RoundService roundService; private final StewardingEntrylistService entrylistService; private final SecurityService securityService; public IncidentDetailView(StewardingIncidentService incidentService, StewardDecisionService decisionService, StewardingAppealService appealService, PenaltyCatalogService catalogService, - ReasoningTemplateService templateService, RaceWeekendService raceWeekendService, - StewardingEntrylistService entrylistService, SecurityService securityService) { + ReasoningTemplateService templateService, SeriesService seriesService, + RoundService roundService, StewardingEntrylistService entrylistService, + SecurityService securityService) { this.incidentService = incidentService; this.decisionService = decisionService; this.appealService = appealService; this.catalogService = catalogService; this.templateService = templateService; - this.raceWeekendService = raceWeekendService; + this.seriesService = seriesService; + this.roundService = roundService; this.entrylistService = entrylistService; this.securityService = securityService; } @@ -71,38 +74,41 @@ public void beforeEnter(BeforeEnterEvent event) { setSpacing(false); removeAll(); - String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); + String seriesIdParam = event.getRouteParameters().get("seriesId").orElse(null); + String roundIdParam = event.getRouteParameters().get("roundId").orElse(null); String incidentIdParam = event.getRouteParameters().get("incidentId").orElse(null); - if (weekendIdParam == null || incidentIdParam == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + if (seriesIdParam == null || roundIdParam == null || incidentIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); return; } - Integer weekendId; + Integer seriesId; + Integer roundId; Integer incidentId; try { - weekendId = Integer.valueOf(weekendIdParam); + seriesId = Integer.valueOf(seriesIdParam); + roundId = Integer.valueOf(roundIdParam); incidentId = Integer.valueOf(incidentIdParam); } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); return; } - RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); + Series series = seriesService.getSeriesById(seriesId); + Round round = roundService.getRoundById(roundId); Incident incident = incidentService.getIncidentById(incidentId); - if (weekend == null || incident == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); + if (series == null || round == null || incident == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); return; } add(createViewHeader(incident.getTitle())); - RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); - Button backButton = new Button("← Back to " + (session != null ? session.getTitle() : "Session"), e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendSessionDetailView.class, + Button backButton = new Button("← Back to " + round.getTitle(), e -> + getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, new RouteParameters( - new RouteParam("weekendId", String.valueOf(weekendId)), - new RouteParam("sessionId", String.valueOf(incident.getSessionId())) + new RouteParam("seriesId", String.valueOf(seriesId)), + new RouteParam("roundId", String.valueOf(roundId)) )))); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); add(backButton); @@ -143,7 +149,7 @@ public void beforeEnter(BeforeEnterEvent event) { // Steward Decision section (ADMIN only) if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - add(createDecisionSection(incident, weekend)); + add(createDecisionSection(incident, series)); } // Decision History @@ -153,7 +159,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(createAppealsSection(incidentId)); } - private VerticalLayout createDecisionSection(Incident incident, RaceWeekend weekend) { + private VerticalLayout createDecisionSection(Incident incident, Series series) { VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.add(new H3("Steward Decision")); @@ -169,29 +175,29 @@ private VerticalLayout createDecisionSection(Incident incident, RaceWeekend week reviseButton.addClickListener(e -> { layout.removeAll(); layout.add(new H3("Revise Decision")); - layout.add(createDecisionForm(incident, weekend, activeDecision.getId())); + layout.add(createDecisionForm(incident, series, activeDecision.getId())); }); layout.add(reviseButton); } else { - layout.add(createDecisionForm(incident, weekend, null)); + layout.add(createDecisionForm(incident, series, null)); } return layout; } - private FormLayout createDecisionForm(Incident incident, RaceWeekend weekend, Integer existingDecisionId) { + private FormLayout createDecisionForm(Incident incident, Series series, Integer existingDecisionId) { FormLayout form = new FormLayout(); form.setResponsiveSteps( new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 2) ); - RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); + RoundSession session = roundService.getSessionById(incident.getSessionId()); ComboBox penaltyCombo = new ComboBox<>("Penalty"); - if (weekend.getPenaltyCatalogId() != null && session != null && session.getSessionType() != null) { + if (series.getPenaltyCatalogId() != null && session != null && session.getSessionType() != null) { List definitions = catalogService.getDefinitionsForSessionType( - weekend.getPenaltyCatalogId(), session.getSessionType().name()); + series.getPenaltyCatalogId(), session.getSessionType().name()); penaltyCombo.setItems(definitions); penaltyCombo.setItemLabelGenerator(d -> d.getCode() + " - " + d.getName()); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java deleted file mode 100644 index 9817fd3a..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendDetailView.java +++ /dev/null @@ -1,388 +0,0 @@ -package de.sustineo.simdesk.views.stewarding; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.checkbox.Checkbox; -import com.vaadin.flow.component.combobox.ComboBox; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; -import com.vaadin.flow.component.datepicker.DatePicker; -import com.vaadin.flow.component.datetimepicker.DateTimePicker; -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.html.H3; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.tabs.TabSheet; -import com.vaadin.flow.component.textfield.IntegerField; -import com.vaadin.flow.component.textfield.TextArea; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteParam; -import com.vaadin.flow.router.RouteParameters; -import com.vaadin.flow.server.auth.AnonymousAllowed; -import de.sustineo.simdesk.configuration.SpringProfile; -import de.sustineo.simdesk.entities.auth.UserRoleEnum; -import de.sustineo.simdesk.entities.stewarding.*; -import de.sustineo.simdesk.layouts.MainLayout; -import de.sustineo.simdesk.services.auth.SecurityService; -import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; -import de.sustineo.simdesk.services.stewarding.RaceWeekendService; -import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; -import de.sustineo.simdesk.services.stewarding.StewardingTrackService; -import de.sustineo.simdesk.views.BaseView; -import org.springframework.context.annotation.Profile; - -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.List; - -@Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/weekends/:weekendId", layout = MainLayout.class) -@AnonymousAllowed -public class RaceWeekendDetailView extends BaseView { - private final RaceWeekendService raceWeekendService; - private final StewardingIncidentService incidentService; - private final StewardingTrackService trackService; - private final PenaltyCatalogService catalogService; - private final SecurityService securityService; - - public RaceWeekendDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, - StewardingTrackService trackService, PenaltyCatalogService catalogService, - SecurityService securityService) { - this.raceWeekendService = raceWeekendService; - this.incidentService = incidentService; - this.trackService = trackService; - this.catalogService = catalogService; - this.securityService = securityService; - } - - @Override - public String getPageTitle() { - return "Race Weekend Details"; - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - setSizeFull(); - setPadding(false); - setSpacing(false); - removeAll(); - - String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); - if (weekendIdParam == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - Integer weekendId; - try { - weekendId = Integer.valueOf(weekendIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); - if (weekend == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - add(createViewHeader(weekend.getTitle())); - - Button backButton = new Button("← Back to Race Weekends", e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class))); - backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - add(backButton); - - VerticalLayout infoLayout = new VerticalLayout(); - infoLayout.setPadding(true); - infoLayout.setSpacing(false); - if (weekend.getDescription() != null && !weekend.getDescription().isEmpty()) { - infoLayout.add(new Span(weekend.getDescription())); - } - if (weekend.getTrack() != null) { - infoLayout.add(createDetailRow("Track", weekend.getTrack().getName())); - } - if (weekend.getPenaltyCatalog() != null) { - infoLayout.add(createDetailRow("Penalty Catalog", weekend.getPenaltyCatalog().getName())); - } - if (weekend.getStartDate() != null && weekend.getEndDate() != null) { - infoLayout.add(createDetailRow("Date", weekend.getStartDate() + " — " + weekend.getEndDate())); - } - infoLayout.add(createDetailRow("Video URL Enabled", Boolean.TRUE.equals(weekend.getVideoUrlEnabled()) ? "Yes" : "No")); - add(infoLayout); - - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - HorizontalLayout weekendActionLayout = new HorizontalLayout(); - Button editWeekendButton = new Button("Edit Weekend"); - editWeekendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - editWeekendButton.addClickListener(e -> openEditWeekendDialog(weekend)); - weekendActionLayout.add(editWeekendButton); - - Button deleteWeekendButton = new Button("Delete Weekend"); - deleteWeekendButton.addThemeVariants(ButtonVariant.LUMO_ERROR); - deleteWeekendButton.addClickListener(e -> { - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setHeader("Delete Weekend"); - confirmDialog.setText("Are you sure you want to delete this weekend?"); - confirmDialog.setCancelable(true); - confirmDialog.setConfirmText("Delete"); - confirmDialog.setConfirmButtonTheme("error primary"); - confirmDialog.addConfirmListener(ev -> { - raceWeekendService.deleteWeekend(weekendId); - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - }); - confirmDialog.open(); - }); - weekendActionLayout.add(deleteWeekendButton); - add(weekendActionLayout); - } - - TabSheet tabSheet = new TabSheet(); - tabSheet.setSizeFull(); - - tabSheet.add("Sessions", createSessionsTab(weekendId)); - tabSheet.add("Incidents", createIncidentsTab(weekendId)); - - addAndExpand(tabSheet); - } - - private HorizontalLayout createDetailRow(String label, String value) { - Span labelSpan = new Span(label + ": "); - labelSpan.getStyle().set("font-weight", "bold"); - Span valueSpan = new Span(value); - HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); - row.setSpacing(false); - row.getStyle().set("gap", "var(--lumo-space-xs)"); - return row; - } - - private VerticalLayout createSessionsTab(Integer weekendId) { - VerticalLayout layout = new VerticalLayout(); - layout.setSizeFull(); - - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - Button addSessionButton = new Button("Add Session"); - addSessionButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - addSessionButton.addClickListener(e -> openAddSessionDialog(weekendId)); - layout.add(addSessionButton); - } - - List sessions = raceWeekendService.getSessionsByWeekendId(weekendId); - Grid grid = new Grid<>(RaceWeekendSession.class, false); - grid.addColumn(session -> session.getSessionType() != null ? session.getSessionType().getDescription() : "-") - .setHeader("Type").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(RaceWeekendSession::getTitle).setHeader("Title").setSortable(true); - grid.addColumn(RaceWeekendSession::getStartTime).setHeader("Start Time").setAutoWidth(true).setFlexGrow(0); - grid.addColumn(RaceWeekendSession::getEndTime).setHeader("End Time").setAutoWidth(true).setFlexGrow(0); - grid.setItems(sessions); - grid.setSizeFull(); - grid.setSelectionMode(Grid.SelectionMode.NONE); - grid.setColumnReorderingAllowed(true); - grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - grid.addItemClickListener(e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendSessionDetailView.class, - new RouteParameters( - new RouteParam("weekendId", String.valueOf(weekendId)), - new RouteParam("sessionId", String.valueOf(e.getItem().getId())) - ))) - ); - - layout.addAndExpand(grid); - return layout; - } - - private void openAddSessionDialog(Integer weekendId) { - Dialog dialog = new Dialog(); - dialog.setHeaderTitle("Add Session"); - dialog.setWidth("600px"); - - FormLayout form = new FormLayout(); - form.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - - ComboBox typeCombo = new ComboBox<>("Session Type"); - typeCombo.setItems(StewSessionType.values()); - typeCombo.setItemLabelGenerator(StewSessionType::getDescription); - typeCombo.setRequired(true); - typeCombo.setWidthFull(); - - TextField titleField = new TextField("Title"); - titleField.setRequired(true); - titleField.setWidthFull(); - - DateTimePicker startTimePicker = new DateTimePicker("Start Time"); - startTimePicker.setWidthFull(); - - DateTimePicker endTimePicker = new DateTimePicker("End Time"); - endTimePicker.setWidthFull(); - - IntegerField sortOrderField = new IntegerField("Sort Order"); - sortOrderField.setMin(0); - sortOrderField.setValue(0); - sortOrderField.setWidthFull(); - - form.add(typeCombo, titleField, startTimePicker, endTimePicker, sortOrderField); - - Button saveButton = new Button("Save", e -> { - if (titleField.isEmpty() || typeCombo.isEmpty()) { - Notification.show("Session type and title are required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - RaceWeekendSession session = RaceWeekendSession.builder() - .raceWeekendId(weekendId) - .sessionType(typeCombo.getValue()) - .title(titleField.getValue()) - .startTime(startTimePicker.getValue() != null ? startTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) - .endTime(endTimePicker.getValue() != null ? endTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) - .sortOrder(sortOrderField.getValue()) - .build(); - - raceWeekendService.createSession(session); - dialog.close(); - Notification.show("Session created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> dialog.close()); - - dialog.add(form); - dialog.getFooter().add(cancelButton, saveButton); - dialog.open(); - } - - private VerticalLayout createIncidentsTab(Integer weekendId) { - VerticalLayout layout = new VerticalLayout(); - layout.setSizeFull(); - - List sessions = raceWeekendService.getSessionsByWeekendId(weekendId); - List allIncidents = new ArrayList<>(); - for (RaceWeekendSession session : sessions) { - allIncidents.addAll(incidentService.getIncidentsBySessionId(session.getId())); - } - - Grid grid = new Grid<>(Incident.class, false); - grid.addColumn(incident -> { - RaceWeekendSession session = raceWeekendService.getSessionById(incident.getSessionId()); - return session != null ? session.getTitle() : "-"; - }).setHeader("Session").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(Incident::getTitle).setHeader("Title").setSortable(true); - grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") - .setHeader("Status").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.setItems(allIncidents); - grid.setSizeFull(); - grid.setSelectionMode(Grid.SelectionMode.NONE); - grid.setColumnReorderingAllowed(true); - grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - - layout.addAndExpand(grid); - return layout; - } - - private void openEditWeekendDialog(RaceWeekend weekend) { - Dialog dialog = new Dialog(); - dialog.setHeaderTitle("Edit Race Weekend"); - dialog.setWidth("700px"); - - FormLayout form = new FormLayout(); - form.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - - TextField titleField = new TextField("Title"); - titleField.setWidthFull(); - titleField.setRequired(true); - titleField.setValue(weekend.getTitle() != null ? weekend.getTitle() : ""); - - TextArea descriptionField = new TextArea("Description"); - descriptionField.setWidthFull(); - descriptionField.setValue(weekend.getDescription() != null ? weekend.getDescription() : ""); - - ComboBox trackCombo = new ComboBox<>("Track"); - List tracks = trackService.getAllTracks(); - trackCombo.setItems(tracks); - trackCombo.setItemLabelGenerator(StewardingTrack::getName); - trackCombo.setWidthFull(); - if (weekend.getTrack() != null) { - tracks.stream().filter(t -> t.getId().equals(weekend.getTrackId())).findFirst().ifPresent(trackCombo::setValue); - } - - ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); - List catalogs = catalogService.getAllCatalogs(); - catalogCombo.setItems(catalogs); - catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); - catalogCombo.setWidthFull(); - if (weekend.getPenaltyCatalog() != null) { - catalogs.stream().filter(c -> c.getId().equals(weekend.getPenaltyCatalogId())).findFirst().ifPresent(catalogCombo::setValue); - } - - TextField webhookField = new TextField("Discord Webhook URL"); - webhookField.setWidthFull(); - webhookField.setValue(weekend.getDiscordWebhookUrl() != null ? weekend.getDiscordWebhookUrl() : ""); - - Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); - videoUrlEnabledCheckbox.setValue(Boolean.TRUE.equals(weekend.getVideoUrlEnabled())); - - DatePicker startDatePicker = new DatePicker("Start Date"); - startDatePicker.setWidthFull(); - startDatePicker.setValue(weekend.getStartDate()); - - DatePicker endDatePicker = new DatePicker("End Date"); - endDatePicker.setWidthFull(); - endDatePicker.setValue(weekend.getEndDate()); - - form.add(titleField, 2); - form.add(descriptionField, 2); - form.add(trackCombo); - form.add(catalogCombo); - form.add(webhookField, 2); - form.add(videoUrlEnabledCheckbox, 2); - form.add(startDatePicker); - form.add(endDatePicker); - - Button saveButton = new Button("Save", e -> { - if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - weekend.setTitle(titleField.getValue()); - weekend.setDescription(descriptionField.getValue()); - weekend.setTrackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null); - weekend.setPenaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null); - weekend.setDiscordWebhookUrl(webhookField.getValue()); - weekend.setVideoUrlEnabled(videoUrlEnabledCheckbox.getValue()); - weekend.setStartDate(startDatePicker.getValue()); - weekend.setEndDate(endDatePicker.getValue()); - - raceWeekendService.updateWeekend(weekend); - dialog.close(); - Notification.show("Weekend updated", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> dialog.close()); - - dialog.add(form); - dialog.getFooter().add(cancelButton, saveButton); - dialog.open(); - } -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java deleted file mode 100644 index b0a3ad45..00000000 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendSessionDetailView.java +++ /dev/null @@ -1,327 +0,0 @@ -package de.sustineo.simdesk.views.stewarding; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.combobox.MultiSelectComboBox; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.html.H3; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.NumberField; -import com.vaadin.flow.component.textfield.TextArea; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.component.upload.Upload; -import com.vaadin.flow.server.streams.UploadHandler; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteParam; -import com.vaadin.flow.router.RouteParameters; -import com.vaadin.flow.server.auth.AnonymousAllowed; -import de.sustineo.simdesk.configuration.SpringProfile; -import de.sustineo.simdesk.entities.auth.UserRoleEnum; -import de.sustineo.simdesk.entities.stewarding.*; -import de.sustineo.simdesk.layouts.MainLayout; -import de.sustineo.simdesk.services.auth.SecurityService; -import de.sustineo.simdesk.services.stewarding.RaceWeekendService; -import de.sustineo.simdesk.services.stewarding.StewardingEntrylistService; -import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; -import de.sustineo.simdesk.views.BaseView; -import org.springframework.context.annotation.Profile; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/weekends/:weekendId/sessions/:sessionId", layout = MainLayout.class) -@AnonymousAllowed -public class RaceWeekendSessionDetailView extends BaseView { - private final RaceWeekendService raceWeekendService; - private final StewardingIncidentService incidentService; - private final StewardingEntrylistService entrylistService; - private final SecurityService securityService; - - public RaceWeekendSessionDetailView(RaceWeekendService raceWeekendService, StewardingIncidentService incidentService, - StewardingEntrylistService entrylistService, SecurityService securityService) { - this.raceWeekendService = raceWeekendService; - this.incidentService = incidentService; - this.entrylistService = entrylistService; - this.securityService = securityService; - } - - @Override - public String getPageTitle() { - return "Session Details"; - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - setSizeFull(); - setPadding(false); - setSpacing(false); - removeAll(); - - String weekendIdParam = event.getRouteParameters().get("weekendId").orElse(null); - String sessionIdParam = event.getRouteParameters().get("sessionId").orElse(null); - if (weekendIdParam == null || sessionIdParam == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - Integer weekendId; - Integer sessionId; - try { - weekendId = Integer.valueOf(weekendIdParam); - sessionId = Integer.valueOf(sessionIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - RaceWeekend weekend = raceWeekendService.getWeekendById(weekendId); - RaceWeekendSession session = raceWeekendService.getSessionById(sessionId); - if (weekend == null || session == null) { - getUI().ifPresent(ui -> ui.navigate(RaceWeekendListView.class)); - return; - } - - add(createViewHeader(session.getTitle())); - - Button backButton = new Button("← Back to " + weekend.getTitle(), e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, - new RouteParameters("weekendId", String.valueOf(weekendId))))); - backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - add(backButton); - - VerticalLayout infoLayout = new VerticalLayout(); - infoLayout.setPadding(true); - infoLayout.setSpacing(false); - if (session.getSessionType() != null) { - infoLayout.add(createDetailRow("Type", session.getSessionType().getDescription())); - } - if (session.getStartTime() != null) { - infoLayout.add(createDetailRow("Start", session.getStartTime().toString())); - } - if (session.getEndTime() != null) { - infoLayout.add(createDetailRow("End", session.getEndTime().toString())); - } - add(infoLayout); - - HorizontalLayout actionLayout = new HorizontalLayout(); - - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD, UserRoleEnum.ROLE_DRIVER)) { - Button reportIncidentButton = new Button("Report Incident"); - reportIncidentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - reportIncidentButton.addClickListener(e -> openReportIncidentDialog(sessionId, weekendId, weekend)); - actionLayout.add(reportIncidentButton); - } - - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - Button quickDecisionButton = new Button("Quick Decision"); - quickDecisionButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); - actionLayout.add(quickDecisionButton); - - Button editSessionButton = new Button("Edit Session"); - editSessionButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - actionLayout.add(editSessionButton); - - Button deleteSessionButton = new Button("Delete Session"); - deleteSessionButton.addThemeVariants(ButtonVariant.LUMO_ERROR); - deleteSessionButton.addClickListener(e -> { - ConfirmDialog confirmDialog = new ConfirmDialog(); - confirmDialog.setHeader("Delete Session"); - confirmDialog.setText("Are you sure you want to delete this session?"); - confirmDialog.setCancelable(true); - confirmDialog.setConfirmText("Delete"); - confirmDialog.setConfirmButtonTheme("error primary"); - confirmDialog.addConfirmListener(ev -> { - raceWeekendService.deleteSession(sessionId); - getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, - new RouteParameters("weekendId", String.valueOf(weekendId)))); - }); - confirmDialog.open(); - }); - actionLayout.add(deleteSessionButton); - } - - if (actionLayout.getComponentCount() > 0) { - add(actionLayout); - } - - List incidents = incidentService.getIncidentsBySessionId(sessionId); - Grid grid = new Grid<>(Incident.class, false); - grid.addColumn(Incident::getTitle).setHeader("Title").setSortable(true); - grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") - .setHeader("Status").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(Incident::getInvolvedCarsText).setHeader("Involved Cars").setAutoWidth(true).setFlexGrow(0); - grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.setItems(incidents); - grid.setSizeFull(); - grid.setSelectionMode(Grid.SelectionMode.NONE); - grid.setColumnReorderingAllowed(true); - grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - grid.addItemClickListener(e -> - getUI().ifPresent(ui -> ui.navigate(IncidentDetailView.class, - new RouteParameters( - new RouteParam("weekendId", String.valueOf(weekendId)), - new RouteParam("incidentId", String.valueOf(e.getItem().getId())) - ))) - ); - - add(grid); - - // Entrylist section - VerticalLayout entrylistLayout = new VerticalLayout(); - entrylistLayout.setPadding(true); - entrylistLayout.add(new H3("Entrylist")); - - if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - Upload upload = new Upload(); - upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { - String json = new String(data, StandardCharsets.UTF_8); - try { - entrylistService.uploadEntrylistForSession(sessionId, weekendId, json); - getUI().ifPresent(ui -> ui.access(() -> { - Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - ui.getPage().reload(); - })); - } catch (IllegalArgumentException ex) { - getUI().ifPresent(ui -> ui.access(() -> - Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR) - )); - } - })); - upload.setAcceptedFileTypes("application/json", ".json"); - upload.setMaxFiles(1); - entrylistLayout.add(upload); - } - - StewardingEntrylist entrylist = entrylistService.getEntrylistBySessionId(sessionId); - if (entrylist != null) { - List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); - Grid entrylistGrid = new Grid<>(StewardingEntrylistEntry.class, false); - entrylistGrid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true).setFlexGrow(0).setSortable(true); - entrylistGrid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setSortable(true); - entrylistGrid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setSortable(true); - entrylistGrid.setItems(entries); - entrylistGrid.setSelectionMode(Grid.SelectionMode.NONE); - entrylistGrid.setColumnReorderingAllowed(true); - entrylistGrid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - entrylistLayout.add(entrylistGrid); - } - - addAndExpand(entrylistLayout); - } - - private HorizontalLayout createDetailRow(String label, String value) { - Span labelSpan = new Span(label + ": "); - labelSpan.getStyle().set("font-weight", "bold"); - Span valueSpan = new Span(value); - HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); - row.setSpacing(false); - row.getStyle().set("gap", "var(--lumo-space-xs)"); - return row; - } - - private void openReportIncidentDialog(Integer sessionId, Integer weekendId, RaceWeekend weekend) { - Dialog dialog = new Dialog(); - dialog.setHeaderTitle("Report Incident"); - dialog.setWidth("600px"); - - FormLayout form = new FormLayout(); - form.setResponsiveSteps( - new FormLayout.ResponsiveStep("0", 1), - new FormLayout.ResponsiveStep("600px", 2) - ); - - TextField titleField = new TextField("Title"); - titleField.setRequired(true); - titleField.setWidthFull(); - - TextArea descriptionField = new TextArea("Description"); - descriptionField.setWidthFull(); - descriptionField.setMinHeight("100px"); - - NumberField mapMarkerXField = new NumberField("Map Marker X"); - mapMarkerXField.setWidthFull(); - - NumberField mapMarkerYField = new NumberField("Map Marker Y"); - mapMarkerYField.setWidthFull(); - - // Populate involved cars from session entrylist - MultiSelectComboBox involvedCarsCombo = new MultiSelectComboBox<>("Involved Cars"); - involvedCarsCombo.setWidthFull(); - StewardingEntrylist entrylist = entrylistService.getEntrylistBySessionId(sessionId); - if (entrylist != null) { - List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); - involvedCarsCombo.setItems(entries); - } - involvedCarsCombo.setItemLabelGenerator(StewardingEntrylistEntry::getDisplayName); - - form.add(titleField, 2); - form.add(descriptionField, 2); - form.add(mapMarkerXField, mapMarkerYField); - form.add(involvedCarsCombo, 2); - - final TextField videoUrlField; - if (Boolean.TRUE.equals(weekend.getVideoUrlEnabled())) { - videoUrlField = new TextField("Video URL"); - videoUrlField.setWidthFull(); - form.add(videoUrlField, 2); - } else { - videoUrlField = null; - } - - Button saveButton = new Button("Report", e -> { - if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - return; - } - - Set selectedEntries = involvedCarsCombo.getValue(); - String involvedCarsText = selectedEntries.stream() - .map(StewardingEntrylistEntry::getDisplayName) - .collect(Collectors.joining(", ")); - List involvedEntryIds = selectedEntries.stream() - .map(StewardingEntrylistEntry::getId) - .collect(Collectors.toList()); - - String videoUrl = videoUrlField != null ? videoUrlField.getValue() : null; - - Incident incident = Incident.builder() - .sessionId(sessionId) - .title(titleField.getValue()) - .description(descriptionField.getValue()) - .mapMarkerX(mapMarkerXField.getValue()) - .mapMarkerY(mapMarkerYField.getValue()) - .involvedCarsText(involvedCarsText) - .videoUrl(videoUrl) - .status(IncidentStatus.REPORTED) - .build(); - - incidentService.createIncident(incident, involvedEntryIds); - dialog.close(); - Notification.show("Incident reported", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); - }); - saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - Button cancelButton = new Button("Cancel", e -> dialog.close()); - - dialog.add(form); - dialog.getFooter().add(cancelButton, saveButton); - dialog.open(); - } -} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java new file mode 100644 index 00000000..5d745efb --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java @@ -0,0 +1,527 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.datetimepicker.DateTimePicker; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.tabs.TabSheet; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.NumberField; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.component.upload.Upload; +import com.vaadin.flow.server.streams.UploadHandler; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParam; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.*; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.RoundService; +import de.sustineo.simdesk.services.stewarding.SeriesService; +import de.sustineo.simdesk.services.stewarding.StewardingEntrylistService; +import de.sustineo.simdesk.services.stewarding.StewardingIncidentService; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/series/:seriesId/rounds/:roundId", layout = MainLayout.class) +@AnonymousAllowed +public class RoundDetailView extends BaseView { + private final SeriesService seriesService; + private final RoundService roundService; + private final StewardingIncidentService incidentService; + private final StewardingEntrylistService entrylistService; + private final StewardingTrackService trackService; + private final SecurityService securityService; + + public RoundDetailView(SeriesService seriesService, RoundService roundService, + StewardingIncidentService incidentService, StewardingEntrylistService entrylistService, + StewardingTrackService trackService, SecurityService securityService) { + this.seriesService = seriesService; + this.roundService = roundService; + this.incidentService = incidentService; + this.entrylistService = entrylistService; + this.trackService = trackService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Round Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String seriesIdParam = event.getRouteParameters().get("seriesId").orElse(null); + String roundIdParam = event.getRouteParameters().get("roundId").orElse(null); + if (seriesIdParam == null || roundIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + Integer seriesId; + Integer roundId; + try { + seriesId = Integer.valueOf(seriesIdParam); + roundId = Integer.valueOf(roundIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + Series series = seriesService.getSeriesById(seriesId); + Round round = roundService.getRoundById(roundId); + if (series == null || round == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + add(createViewHeader(round.getTitle())); + + Button backButton = new Button("← Back to " + series.getTitle(), e -> + getUI().ifPresent(ui -> ui.navigate(SeriesDetailView.class, + new RouteParameters("seriesId", String.valueOf(seriesId))))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + + VerticalLayout infoLayout = new VerticalLayout(); + infoLayout.setPadding(true); + infoLayout.setSpacing(false); + if (round.getTrack() != null) { + infoLayout.add(createDetailRow("Track", round.getTrack().getName())); + } + if (round.getStartDate() != null && round.getEndDate() != null) { + infoLayout.add(createDetailRow("Date", round.getStartDate() + " — " + round.getEndDate())); + } + add(infoLayout); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + HorizontalLayout actionLayout = new HorizontalLayout(); + Button editButton = new Button("Edit Round"); + editButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + editButton.addClickListener(e -> openEditRoundDialog(round, seriesId)); + actionLayout.add(editButton); + + Button deleteButton = new Button("Delete Round"); + deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + deleteButton.addClickListener(e -> { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Delete Round"); + confirmDialog.setText("Are you sure you want to delete this round?"); + confirmDialog.setCancelable(true); + confirmDialog.setConfirmText("Delete"); + confirmDialog.setConfirmButtonTheme("error primary"); + confirmDialog.addConfirmListener(ev -> { + roundService.deleteRound(roundId); + getUI().ifPresent(ui -> ui.navigate(SeriesDetailView.class, + new RouteParameters("seriesId", String.valueOf(seriesId)))); + }); + confirmDialog.open(); + }); + actionLayout.add(deleteButton); + add(actionLayout); + } + + TabSheet tabSheet = new TabSheet(); + tabSheet.setSizeFull(); + + tabSheet.add("Sessions", createSessionsTab(roundId)); + tabSheet.add("Incidents", createIncidentsTab(seriesId, roundId, series)); + tabSheet.add("Entrylist", createEntrylistTab(roundId)); + + addAndExpand(tabSheet); + } + + private HorizontalLayout createDetailRow(String label, String value) { + Span labelSpan = new Span(label + ": "); + labelSpan.getStyle().set("font-weight", "bold"); + Span valueSpan = new Span(value); + HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); + row.setSpacing(false); + row.getStyle().set("gap", "var(--lumo-space-xs)"); + return row; + } + + private VerticalLayout createSessionsTab(Integer roundId) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + Button addSessionButton = new Button("Add Session"); + addSessionButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + addSessionButton.addClickListener(e -> openAddSessionDialog(roundId)); + layout.add(addSessionButton); + } + + List sessions = roundService.getSessionsByRoundId(roundId); + Grid grid = new Grid<>(RoundSession.class, false); + grid.addColumn(session -> session.getSessionType() != null ? session.getSessionType().getDescription() : "-") + .setHeader("Type").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(RoundSession::getTitle).setHeader("Title").setSortable(true); + grid.addColumn(RoundSession::getStartTime).setHeader("Start Time").setAutoWidth(true).setFlexGrow(0); + grid.addColumn(RoundSession::getEndTime).setHeader("End Time").setAutoWidth(true).setFlexGrow(0); + grid.setItems(sessions); + grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + + layout.addAndExpand(grid); + return layout; + } + + private void openAddSessionDialog(Integer roundId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Add Session"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + ComboBox typeCombo = new ComboBox<>("Session Type"); + typeCombo.setItems(StewSessionType.values()); + typeCombo.setItemLabelGenerator(StewSessionType::getDescription); + typeCombo.setRequired(true); + typeCombo.setWidthFull(); + + TextField titleField = new TextField("Title"); + titleField.setRequired(true); + titleField.setWidthFull(); + + DateTimePicker startTimePicker = new DateTimePicker("Start Time"); + startTimePicker.setWidthFull(); + + DateTimePicker endTimePicker = new DateTimePicker("End Time"); + endTimePicker.setWidthFull(); + + IntegerField sortOrderField = new IntegerField("Sort Order"); + sortOrderField.setMin(0); + sortOrderField.setValue(0); + sortOrderField.setWidthFull(); + + form.add(typeCombo, titleField, startTimePicker, endTimePicker, sortOrderField); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty() || typeCombo.isEmpty()) { + Notification.show("Session type and title are required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + RoundSession session = RoundSession.builder() + .roundId(roundId) + .sessionType(typeCombo.getValue()) + .title(titleField.getValue()) + .startTime(startTimePicker.getValue() != null ? startTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) + .endTime(endTimePicker.getValue() != null ? endTimePicker.getValue().toInstant(ZoneOffset.UTC) : null) + .sortOrder(sortOrderField.getValue()) + .build(); + + roundService.createSession(session); + dialog.close(); + Notification.show("Session created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } + + private VerticalLayout createIncidentsTab(Integer seriesId, Integer roundId, Series series) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD, UserRoleEnum.ROLE_DRIVER)) { + Button reportButton = new Button("Report Incident"); + reportButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + reportButton.addClickListener(e -> openReportIncidentDialog(seriesId, roundId, series)); + layout.add(reportButton); + } + + List sessions = roundService.getSessionsByRoundId(roundId); + List allIncidents = new ArrayList<>(); + for (RoundSession session : sessions) { + allIncidents.addAll(incidentService.getIncidentsBySessionId(session.getId())); + } + + Grid grid = new Grid<>(Incident.class, false); + grid.addColumn(incident -> { + RoundSession session = roundService.getSessionById(incident.getSessionId()); + return session != null ? session.getTitle() : "-"; + }).setHeader("Session").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Incident::getTitle).setHeader("Title").setSortable(true); + grid.addColumn(incident -> incident.getStatus() != null ? incident.getStatus().getDescription() : "-") + .setHeader("Status").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Incident::getCreatedAt).setHeader("Created").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.setItems(allIncidents); + grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(IncidentDetailView.class, + new RouteParameters( + new RouteParam("seriesId", String.valueOf(seriesId)), + new RouteParam("roundId", String.valueOf(roundId)), + new RouteParam("incidentId", String.valueOf(e.getItem().getId())) + ))) + ); + + layout.addAndExpand(grid); + return layout; + } + + private void openReportIncidentDialog(Integer seriesId, Integer roundId, Series series) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Report Incident"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + List sessions = roundService.getSessionsByRoundId(roundId); + ComboBox sessionCombo = new ComboBox<>("Session"); + sessionCombo.setItems(sessions); + sessionCombo.setItemLabelGenerator(RoundSession::getTitle); + sessionCombo.setRequired(true); + sessionCombo.setWidthFull(); + + TextField titleField = new TextField("Title"); + titleField.setRequired(true); + titleField.setWidthFull(); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + descriptionField.setMinHeight("100px"); + + NumberField mapMarkerXField = new NumberField("Map Marker X"); + mapMarkerXField.setWidthFull(); + + NumberField mapMarkerYField = new NumberField("Map Marker Y"); + mapMarkerYField.setWidthFull(); + + MultiSelectComboBox involvedCarsCombo = new MultiSelectComboBox<>("Involved Cars"); + involvedCarsCombo.setWidthFull(); + StewardingEntrylist entrylist = entrylistService.getEntrylistByRoundId(roundId); + if (entrylist != null) { + List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); + involvedCarsCombo.setItems(entries); + } + involvedCarsCombo.setItemLabelGenerator(StewardingEntrylistEntry::getDisplayName); + + form.add(sessionCombo, 2); + form.add(titleField, 2); + form.add(descriptionField, 2); + form.add(mapMarkerXField, mapMarkerYField); + form.add(involvedCarsCombo, 2); + + final TextField videoUrlField; + if (Boolean.TRUE.equals(series.getVideoUrlEnabled())) { + videoUrlField = new TextField("Video URL"); + videoUrlField.setWidthFull(); + form.add(videoUrlField, 2); + } else { + videoUrlField = null; + } + + Button saveButton = new Button("Report", e -> { + if (sessionCombo.isEmpty()) { + Notification.show("Session is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + Set selectedEntries = involvedCarsCombo.getValue(); + String involvedCarsText = selectedEntries.stream() + .map(StewardingEntrylistEntry::getDisplayName) + .collect(Collectors.joining(", ")); + List involvedEntryIds = selectedEntries.stream() + .map(StewardingEntrylistEntry::getId) + .collect(Collectors.toList()); + + String videoUrl = videoUrlField != null ? videoUrlField.getValue() : null; + + Incident incident = Incident.builder() + .sessionId(sessionCombo.getValue().getId()) + .title(titleField.getValue()) + .description(descriptionField.getValue()) + .mapMarkerX(mapMarkerXField.getValue()) + .mapMarkerY(mapMarkerYField.getValue()) + .involvedCarsText(involvedCarsText) + .videoUrl(videoUrl) + .status(IncidentStatus.REPORTED) + .build(); + + incidentService.createIncident(incident, involvedEntryIds); + dialog.close(); + Notification.show("Incident reported", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } + + private VerticalLayout createEntrylistTab(Integer roundId) { + VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.add(new H3("Entrylist")); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + Upload upload = new Upload(); + upload.setUploadHandler(UploadHandler.inMemory((metadata, data) -> { + String json = new String(data, StandardCharsets.UTF_8); + try { + entrylistService.uploadEntrylistForRound(roundId, json); + getUI().ifPresent(ui -> ui.access(() -> { + Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + ui.getPage().reload(); + })); + } catch (IllegalArgumentException ex) { + getUI().ifPresent(ui -> ui.access(() -> + Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR) + )); + } + })); + upload.setAcceptedFileTypes("application/json", ".json"); + upload.setMaxFiles(1); + layout.add(upload); + } + + StewardingEntrylist entrylist = entrylistService.getEntrylistByRoundId(roundId); + if (entrylist != null) { + List entries = entrylistService.getEntriesByEntrylistId(entrylist.getId()); + Grid entrylistGrid = new Grid<>(StewardingEntrylistEntry.class, false); + entrylistGrid.addColumn(StewardingEntrylistEntry::getRaceNumber).setHeader("Race Number").setAutoWidth(true).setFlexGrow(0).setSortable(true); + entrylistGrid.addColumn(StewardingEntrylistEntry::getTeamName).setHeader("Team Name").setSortable(true); + entrylistGrid.addColumn(StewardingEntrylistEntry::getDisplayName).setHeader("Display Name").setSortable(true); + entrylistGrid.setItems(entries); + entrylistGrid.setSelectionMode(Grid.SelectionMode.NONE); + entrylistGrid.setColumnReorderingAllowed(true); + entrylistGrid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + layout.addAndExpand(entrylistGrid); + } + + return layout; + } + + private void openEditRoundDialog(Round round, Integer seriesId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Edit Round"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + titleField.setValue(round.getTitle() != null ? round.getTitle() : ""); + + ComboBox trackCombo = new ComboBox<>("Track"); + List tracks = trackService.getAllTracks(); + trackCombo.setItems(tracks); + trackCombo.setItemLabelGenerator(StewardingTrack::getName); + trackCombo.setWidthFull(); + if (round.getTrack() != null) { + tracks.stream().filter(t -> t.getId().equals(round.getTrackId())).findFirst().ifPresent(trackCombo::setValue); + } + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + startDatePicker.setValue(round.getStartDate()); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + endDatePicker.setValue(round.getEndDate()); + + form.add(titleField, 2); + form.add(trackCombo, 2); + form.add(startDatePicker); + form.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + round.setTitle(titleField.getValue()); + round.setTrackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null); + round.setStartDate(startDatePicker.getValue()); + round.setEndDate(endDatePicker.getValue()); + + roundService.updateRound(round); + dialog.close(); + Notification.show("Round updated", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java new file mode 100644 index 00000000..1a0b4ff8 --- /dev/null +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java @@ -0,0 +1,330 @@ +package de.sustineo.simdesk.views.stewarding; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridVariant; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParam; +import com.vaadin.flow.router.RouteParameters; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import de.sustineo.simdesk.configuration.SpringProfile; +import de.sustineo.simdesk.entities.auth.UserRoleEnum; +import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; +import de.sustineo.simdesk.entities.stewarding.Round; +import de.sustineo.simdesk.entities.stewarding.Series; +import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.auth.SecurityService; +import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; +import de.sustineo.simdesk.services.stewarding.RoundService; +import de.sustineo.simdesk.services.stewarding.SeriesService; +import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.views.BaseView; +import org.springframework.context.annotation.Profile; + +import java.util.List; + +@Profile(SpringProfile.STEWARDING) +@Route(value = "/stewarding/series/:seriesId", layout = MainLayout.class) +@AnonymousAllowed +public class SeriesDetailView extends BaseView { + private final SeriesService seriesService; + private final RoundService roundService; + private final StewardingTrackService trackService; + private final PenaltyCatalogService catalogService; + private final SecurityService securityService; + + public SeriesDetailView(SeriesService seriesService, RoundService roundService, + StewardingTrackService trackService, PenaltyCatalogService catalogService, + SecurityService securityService) { + this.seriesService = seriesService; + this.roundService = roundService; + this.trackService = trackService; + this.catalogService = catalogService; + this.securityService = securityService; + } + + @Override + public String getPageTitle() { + return "Series Details"; + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + setSizeFull(); + setPadding(false); + setSpacing(false); + removeAll(); + + String seriesIdParam = event.getRouteParameters().get("seriesId").orElse(null); + if (seriesIdParam == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + Integer seriesId; + try { + seriesId = Integer.valueOf(seriesIdParam); + } catch (NumberFormatException e) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + Series series = seriesService.getSeriesById(seriesId); + if (series == null) { + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + return; + } + + add(createViewHeader(series.getTitle())); + + Button backButton = new Button("← Back to Series", e -> + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class))); + backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + add(backButton); + + VerticalLayout infoLayout = new VerticalLayout(); + infoLayout.setPadding(true); + infoLayout.setSpacing(false); + if (series.getDescription() != null && !series.getDescription().isEmpty()) { + infoLayout.add(new Span(series.getDescription())); + } + if (series.getPenaltyCatalog() != null) { + infoLayout.add(createDetailRow("Penalty Catalog", series.getPenaltyCatalog().getName())); + } + if (series.getStartDate() != null && series.getEndDate() != null) { + infoLayout.add(createDetailRow("Date", series.getStartDate() + " — " + series.getEndDate())); + } + infoLayout.add(createDetailRow("Video URL Enabled", Boolean.TRUE.equals(series.getVideoUrlEnabled()) ? "Yes" : "No")); + add(infoLayout); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + HorizontalLayout actionLayout = new HorizontalLayout(); + Button editButton = new Button("Edit Series"); + editButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + editButton.addClickListener(e -> openEditSeriesDialog(series)); + actionLayout.add(editButton); + + Button deleteButton = new Button("Delete Series"); + deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR); + deleteButton.addClickListener(e -> { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Delete Series"); + confirmDialog.setText("Are you sure you want to delete this series?"); + confirmDialog.setCancelable(true); + confirmDialog.setConfirmText("Delete"); + confirmDialog.setConfirmButtonTheme("error primary"); + confirmDialog.addConfirmListener(ev -> { + seriesService.deleteSeries(seriesId); + getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); + }); + confirmDialog.open(); + }); + actionLayout.add(deleteButton); + add(actionLayout); + } + + // Rounds grid + HorizontalLayout roundsHeader = new HorizontalLayout(); + roundsHeader.setWidthFull(); + roundsHeader.setAlignItems(Alignment.CENTER); + + if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { + Button newRoundButton = new Button("New Round", e -> openNewRoundDialog(seriesId)); + newRoundButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + roundsHeader.add(newRoundButton); + } + + add(roundsHeader); + + List rounds = roundService.getRoundsBySeriesId(seriesId); + Grid grid = new Grid<>(Round.class, false); + grid.addColumn(Round::getTitle).setHeader("Title").setSortable(true); + grid.addColumn(round -> round.getTrack() != null ? round.getTrack().getName() : "-") + .setHeader("Track").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Round::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Round::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.setItems(rounds); + grid.setSizeFull(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.setColumnReorderingAllowed(true); + grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + grid.addItemClickListener(e -> + getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, + new RouteParameters( + new RouteParam("seriesId", String.valueOf(seriesId)), + new RouteParam("roundId", String.valueOf(e.getItem().getId())) + ))) + ); + + addAndExpand(grid); + } + + private HorizontalLayout createDetailRow(String label, String value) { + Span labelSpan = new Span(label + ": "); + labelSpan.getStyle().set("font-weight", "bold"); + Span valueSpan = new Span(value); + HorizontalLayout row = new HorizontalLayout(labelSpan, valueSpan); + row.setSpacing(false); + row.getStyle().set("gap", "var(--lumo-space-xs)"); + return row; + } + + private void openEditSeriesDialog(Series series) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Edit Series"); + dialog.setWidth("700px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + titleField.setValue(series.getTitle() != null ? series.getTitle() : ""); + + TextArea descriptionField = new TextArea("Description"); + descriptionField.setWidthFull(); + descriptionField.setValue(series.getDescription() != null ? series.getDescription() : ""); + + ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); + List catalogs = catalogService.getAllCatalogs(); + catalogCombo.setItems(catalogs); + catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); + catalogCombo.setWidthFull(); + if (series.getPenaltyCatalog() != null) { + catalogs.stream().filter(c -> c.getId().equals(series.getPenaltyCatalogId())).findFirst().ifPresent(catalogCombo::setValue); + } + + TextField webhookField = new TextField("Discord Webhook URL"); + webhookField.setWidthFull(); + webhookField.setValue(series.getDiscordWebhookUrl() != null ? series.getDiscordWebhookUrl() : ""); + + Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); + videoUrlEnabledCheckbox.setValue(Boolean.TRUE.equals(series.getVideoUrlEnabled())); + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + startDatePicker.setValue(series.getStartDate()); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + endDatePicker.setValue(series.getEndDate()); + + form.add(titleField, 2); + form.add(descriptionField, 2); + form.add(catalogCombo, 2); + form.add(webhookField, 2); + form.add(videoUrlEnabledCheckbox, 2); + form.add(startDatePicker); + form.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + series.setTitle(titleField.getValue()); + series.setDescription(descriptionField.getValue()); + series.setPenaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null); + series.setDiscordWebhookUrl(webhookField.getValue()); + series.setVideoUrlEnabled(videoUrlEnabledCheckbox.getValue()); + series.setStartDate(startDatePicker.getValue()); + series.setEndDate(endDatePicker.getValue()); + + seriesService.updateSeries(series); + dialog.close(); + Notification.show("Series updated", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } + + private void openNewRoundDialog(Integer seriesId) { + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("New Round"); + dialog.setWidth("600px"); + + FormLayout form = new FormLayout(); + form.setResponsiveSteps( + new FormLayout.ResponsiveStep("0", 1), + new FormLayout.ResponsiveStep("600px", 2) + ); + + TextField titleField = new TextField("Title"); + titleField.setWidthFull(); + titleField.setRequired(true); + + ComboBox trackCombo = new ComboBox<>("Track"); + trackCombo.setItems(trackService.getAllTracks()); + trackCombo.setItemLabelGenerator(StewardingTrack::getName); + trackCombo.setWidthFull(); + + DatePicker startDatePicker = new DatePicker("Start Date"); + startDatePicker.setWidthFull(); + + DatePicker endDatePicker = new DatePicker("End Date"); + endDatePicker.setWidthFull(); + + form.add(titleField, 2); + form.add(trackCombo, 2); + form.add(startDatePicker); + form.add(endDatePicker); + + Button saveButton = new Button("Save", e -> { + if (titleField.isEmpty()) { + Notification.show("Title is required", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + return; + } + + Round round = Round.builder() + .seriesId(seriesId) + .title(titleField.getValue()) + .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) + .startDate(startDatePicker.getValue()) + .endDate(endDatePicker.getValue()) + .build(); + + roundService.createRound(round); + dialog.close(); + Notification.show("Round created", 3000, Notification.Position.MIDDLE) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.getPage().reload()); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + Button cancelButton = new Button("Cancel", e -> dialog.close()); + + dialog.add(form); + dialog.getFooter().add(cancelButton, saveButton); + dialog.open(); + } +} diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java similarity index 67% rename from simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java rename to simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java index dce9b2a9..8e027055 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RaceWeekendListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java @@ -21,38 +21,34 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.auth.UserRoleEnum; import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; -import de.sustineo.simdesk.entities.stewarding.RaceWeekend; -import de.sustineo.simdesk.entities.stewarding.StewardingTrack; +import de.sustineo.simdesk.entities.stewarding.Series; import de.sustineo.simdesk.layouts.MainLayout; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; -import de.sustineo.simdesk.services.stewarding.RaceWeekendService; -import de.sustineo.simdesk.services.stewarding.StewardingTrackService; +import de.sustineo.simdesk.services.stewarding.SeriesService; import de.sustineo.simdesk.views.BaseView; import org.springframework.context.annotation.Profile; import java.util.List; @Profile(SpringProfile.STEWARDING) -@Route(value = "/stewarding/weekends", layout = MainLayout.class) +@Route(value = "/stewarding/series", layout = MainLayout.class) @AnonymousAllowed -public class RaceWeekendListView extends BaseView { - private final RaceWeekendService raceWeekendService; - private final StewardingTrackService trackService; +public class SeriesListView extends BaseView { + private final SeriesService seriesService; private final PenaltyCatalogService catalogService; private final SecurityService securityService; - public RaceWeekendListView(RaceWeekendService raceWeekendService, StewardingTrackService trackService, - PenaltyCatalogService catalogService, SecurityService securityService) { - this.raceWeekendService = raceWeekendService; - this.trackService = trackService; + public SeriesListView(SeriesService seriesService, PenaltyCatalogService catalogService, + SecurityService securityService) { + this.seriesService = seriesService; this.catalogService = catalogService; this.securityService = securityService; } @Override public String getPageTitle() { - return "Race Weekends"; + return "Series"; } @Override @@ -68,38 +64,34 @@ public void beforeEnter(BeforeEnterEvent event) { headerLayout.add(createViewHeader()); if (securityService.hasAnyAuthority(UserRoleEnum.ROLE_ADMIN, UserRoleEnum.ROLE_STEWARD)) { - Button newButton = new Button("New Weekend", e -> openNewWeekendDialog()); + Button newButton = new Button("New Series", e -> openNewSeriesDialog()); newButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); headerLayout.add(newButton); } add(headerLayout); - List weekends = raceWeekendService.getAllWeekends(); - Grid grid = new Grid<>(RaceWeekend.class, false); - grid.addColumn(RaceWeekend::getTitle).setHeader("Title").setSortable(true); - grid.addColumn(weekend -> { - var track = trackService.getTrackById(weekend.getTrackId()); - return track != null ? track.getName() : "-"; - }).setHeader("Track").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(RaceWeekend::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(RaceWeekend::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.setItems(weekends); + List seriesList = seriesService.getAllSeries(); + Grid grid = new Grid<>(Series.class, false); + grid.addColumn(Series::getTitle).setHeader("Title").setSortable(true); + grid.addColumn(Series::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.addColumn(Series::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + grid.setItems(seriesList); grid.setSizeFull(); grid.setSelectionMode(Grid.SelectionMode.NONE); grid.setColumnReorderingAllowed(true); grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addItemClickListener(e -> - getUI().ifPresent(ui -> ui.navigate(RaceWeekendDetailView.class, - new RouteParameters("weekendId", String.valueOf(e.getItem().getId())))) + getUI().ifPresent(ui -> ui.navigate(SeriesDetailView.class, + new RouteParameters("seriesId", String.valueOf(e.getItem().getId())))) ); addAndExpand(grid); } - private void openNewWeekendDialog() { + private void openNewSeriesDialog() { Dialog dialog = new Dialog(); - dialog.setHeaderTitle("New Race Weekend"); + dialog.setHeaderTitle("New Series"); dialog.setWidth("700px"); FormLayout form = new FormLayout(); @@ -115,11 +107,6 @@ private void openNewWeekendDialog() { TextArea descriptionField = new TextArea("Description"); descriptionField.setWidthFull(); - ComboBox trackCombo = new ComboBox<>("Track"); - trackCombo.setItems(trackService.getAllTracks()); - trackCombo.setItemLabelGenerator(StewardingTrack::getName); - trackCombo.setWidthFull(); - ComboBox catalogCombo = new ComboBox<>("Penalty Catalog"); catalogCombo.setItems(catalogService.getAllCatalogs()); catalogCombo.setItemLabelGenerator(PenaltyCatalog::getName); @@ -128,6 +115,8 @@ private void openNewWeekendDialog() { TextField webhookField = new TextField("Discord Webhook URL"); webhookField.setWidthFull(); + Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); + DatePicker startDatePicker = new DatePicker("Start Date"); startDatePicker.setWidthFull(); @@ -136,10 +125,8 @@ private void openNewWeekendDialog() { form.add(titleField, 2); form.add(descriptionField, 2); - form.add(trackCombo); - form.add(catalogCombo); + form.add(catalogCombo, 2); form.add(webhookField, 2); - Checkbox videoUrlEnabledCheckbox = new Checkbox("Enable Video URL for incident reports"); form.add(videoUrlEnabledCheckbox, 2); form.add(startDatePicker); form.add(endDatePicker); @@ -151,10 +138,9 @@ private void openNewWeekendDialog() { return; } - RaceWeekend weekend = RaceWeekend.builder() + Series series = Series.builder() .title(titleField.getValue()) .description(descriptionField.getValue()) - .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) .penaltyCatalogId(catalogCombo.getValue() != null ? catalogCombo.getValue().getId() : null) .discordWebhookUrl(webhookField.getValue()) .videoUrlEnabled(videoUrlEnabledCheckbox.getValue()) @@ -162,9 +148,9 @@ private void openNewWeekendDialog() { .endDate(endDatePicker.getValue()) .build(); - raceWeekendService.createWeekend(weekend); + seriesService.createSeries(series); dialog.close(); - Notification.show("Race weekend created", 3000, Notification.Position.MIDDLE) + Notification.show("Series created", 3000, Notification.Position.MIDDLE) .addThemeVariants(NotificationVariant.LUMO_SUCCESS); getUI().ifPresent(ui -> ui.getPage().reload()); }); From ba3c6c6740d3e656b44fe0d785a8d8c96bef1d1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:43:54 +0000 Subject: [PATCH 17/24] =?UTF-8?q?Restructure=20stewarding:=20Series=20?= =?UTF-8?q?=E2=86=92=20Round=20=E2=86=92=20Session=20hierarchy=20(schema-o?= =?UTF-8?q?nly=20migration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../StewardingEntrylistServiceTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java index 546925e0..0c57d933 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java @@ -40,8 +40,8 @@ class StewardingEntrylistServiceTest { private StewardingEntrylistDriverMapper entrylistDriverMapper; @Test - void uploadEntrylist_shouldParseValidAccJson() { - when(entrylistMapper.findByRaceWeekendId(1)).thenReturn(Collections.emptyList()); + void uploadEntrylistForRound_shouldParseValidAccJson() { + when(entrylistMapper.findByRoundId(1)).thenReturn(Collections.emptyList()); String accJson = """ { @@ -86,17 +86,17 @@ void uploadEntrylist_shouldParseValidAccJson() { } """; - entrylistService.uploadEntrylist(1, accJson); + entrylistService.uploadEntrylistForRound(1, accJson); - verify(entrylistMapper).deleteByRaceWeekendId(1); + verify(entrylistMapper).deleteByRoundId(1); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, times(2)).insert(any(StewardingEntrylistEntry.class)); verify(entrylistDriverMapper, times(3)).insert(any(StewardingEntrylistDriver.class)); } @Test - void uploadEntrylist_shouldHandleEmptyEntries() { - when(entrylistMapper.findByRaceWeekendId(1)).thenReturn(Collections.emptyList()); + void uploadEntrylistForRound_shouldHandleEmptyEntries() { + when(entrylistMapper.findByRoundId(1)).thenReturn(Collections.emptyList()); String accJson = """ { @@ -105,9 +105,9 @@ void uploadEntrylist_shouldHandleEmptyEntries() { } """; - entrylistService.uploadEntrylist(1, accJson); + entrylistService.uploadEntrylistForRound(1, accJson); - verify(entrylistMapper).deleteByRaceWeekendId(1); + verify(entrylistMapper).deleteByRoundId(1); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, never()).insert(any()); verify(entrylistDriverMapper, never()).insert(any()); From c1e951c3bad3a16ecd317f9bbc1d6429b2d82243 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:34:24 +0000 Subject: [PATCH 18/24] Add Flyway V2_15_0 migration: drop/recreate stewarding tables with VARCHAR(12) PKs and stewarding_round Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../V2_15_0__stewarding_string_ids.sql | 225 ++++++++++++++++++ .../sqlite/V2_15_0__stewarding_string_ids.sql | 225 ++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql create mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql new file mode 100644 index 00000000..5d673325 --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql @@ -0,0 +1,225 @@ +-- Drop all stewarding tables and recreate with string IDs and stewarding_round table name + +DROP TABLE IF EXISTS simdesk.stewarding_appeal; +DROP TABLE IF EXISTS simdesk.stewarding_decision; +DROP TABLE IF EXISTS simdesk.stewarding_incident_involved_entry; +DROP TABLE IF EXISTS simdesk.stewarding_incident; +DROP TABLE IF EXISTS simdesk.stewarding_entrylist_driver; +DROP TABLE IF EXISTS simdesk.stewarding_entrylist_entry; +DROP TABLE IF EXISTS simdesk.stewarding_entrylist; +DROP TABLE IF EXISTS simdesk.stewarding_session; +DROP TABLE IF EXISTS simdesk.stewarding_race_weekend; +DROP TABLE IF EXISTS simdesk.stewarding_series; +DROP TABLE IF EXISTS simdesk.stewarding_penalty_definition; +DROP TABLE IF EXISTS simdesk.stewarding_penalty_catalog; +DROP TABLE IF EXISTS simdesk.stewarding_reasoning_template; +DROP TABLE IF EXISTS simdesk.stewarding_track; + +CREATE TABLE simdesk.stewarding_track +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + country VARCHAR(100), + map_image_url VARCHAR(500), + map_metadata TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE simdesk.stewarding_penalty_catalog +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE simdesk.stewarding_penalty_definition +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + catalog_id VARCHAR(12) NOT NULL, + code VARCHAR(50), + name VARCHAR(255) NOT NULL, + description TEXT, + category VARCHAR(100), + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + default_penalty VARCHAR(255), + severity INTEGER, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON simdesk.stewarding_penalty_definition (catalog_id); + +CREATE TABLE simdesk.stewarding_reasoning_template +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE simdesk.stewarding_series +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id VARCHAR(12), + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) +); + +CREATE TABLE simdesk.stewarding_round +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + series_id VARCHAR(12), + track_id VARCHAR(12), + title VARCHAR(255) NOT NULL, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (series_id) REFERENCES simdesk.stewarding_series (id), + FOREIGN KEY (track_id) REFERENCES simdesk.stewarding_track (id) +); + +CREATE INDEX ix_stewarding_round_series_id ON simdesk.stewarding_round (series_id); +CREATE INDEX ix_stewarding_round_track_id ON simdesk.stewarding_round (track_id); + +CREATE TABLE simdesk.stewarding_session +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12) NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) +); + +CREATE INDEX ix_stewarding_session_round_id ON simdesk.stewarding_session (round_id); + +CREATE TABLE simdesk.stewarding_entrylist +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12), + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) +); + +CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); + +CREATE TABLE simdesk.stewarding_entrylist_entry +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + entrylist_id VARCHAR(12) NOT NULL, + race_number INTEGER NOT NULL, + car_model_id INTEGER, + team_name VARCHAR(255), + display_name VARCHAR(255), + FOREIGN KEY (entrylist_id) REFERENCES simdesk.stewarding_entrylist (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON simdesk.stewarding_entrylist_entry (entrylist_id); + +CREATE TABLE simdesk.stewarding_entrylist_driver +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + entry_id VARCHAR(12) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + short_name VARCHAR(10), + steam_id VARCHAR(50), + category INTEGER, + FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON simdesk.stewarding_entrylist_driver (entry_id); + +CREATE TABLE simdesk.stewarding_incident +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + session_id VARCHAR(12) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + lap INTEGER, + timestamp_in_session VARCHAR(100), + map_marker_x DOUBLE PRECISION, + map_marker_y DOUBLE PRECISION, + video_url VARCHAR(500), + involved_cars_text VARCHAR(500), + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + reported_by_user_id INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id) +); + +CREATE INDEX ix_stewarding_incident_session_id ON simdesk.stewarding_incident (session_id); +CREATE INDEX ix_stewarding_incident_status ON simdesk.stewarding_incident (status); + +CREATE TABLE simdesk.stewarding_incident_involved_entry +( + incident_id VARCHAR(12) NOT NULL, + entry_id VARCHAR(12) NOT NULL, + PRIMARY KEY (incident_id, entry_id), + FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id) ON DELETE CASCADE, + FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE TABLE simdesk.stewarding_decision +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + incident_id VARCHAR(12), + session_id VARCHAR(12) NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id VARCHAR(12), + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id VARCHAR(12), + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id VARCHAR(12), + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id VARCHAR(12), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id), + FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id), + FOREIGN KEY (penalty_definition_id) REFERENCES simdesk.stewarding_penalty_definition (id), + FOREIGN KEY (reasoning_template_id) REFERENCES simdesk.stewarding_reasoning_template (id), + FOREIGN KEY (penalized_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id), + FOREIGN KEY (superseded_by_id) REFERENCES simdesk.stewarding_decision (id) +); + +CREATE INDEX ix_stewarding_decision_incident_id ON simdesk.stewarding_decision (incident_id); +CREATE INDEX ix_stewarding_decision_session_id ON simdesk.stewarding_decision (session_id); +CREATE INDEX ix_stewarding_decision_is_active ON simdesk.stewarding_decision (is_active); + +CREATE TABLE simdesk.stewarding_appeal +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + decision_id VARCHAR(12) NOT NULL, + filed_by_user_id INTEGER, + filed_by_entry_id VARCHAR(12), + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + response TEXT, + responded_by_user_id INTEGER, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + responded_at TIMESTAMP, + FOREIGN KEY (decision_id) REFERENCES simdesk.stewarding_decision (id), + FOREIGN KEY (filed_by_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) +); + +CREATE INDEX ix_stewarding_appeal_decision_id ON simdesk.stewarding_appeal (decision_id); +CREATE INDEX ix_stewarding_appeal_status ON simdesk.stewarding_appeal (status); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql new file mode 100644 index 00000000..f051a1ff --- /dev/null +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql @@ -0,0 +1,225 @@ +-- Drop all stewarding tables and recreate with string IDs and stewarding_round table name + +DROP TABLE IF EXISTS stewarding_appeal; +DROP TABLE IF EXISTS stewarding_decision; +DROP TABLE IF EXISTS stewarding_incident_involved_entry; +DROP TABLE IF EXISTS stewarding_incident; +DROP TABLE IF EXISTS stewarding_entrylist_driver; +DROP TABLE IF EXISTS stewarding_entrylist_entry; +DROP TABLE IF EXISTS stewarding_entrylist; +DROP TABLE IF EXISTS stewarding_session; +DROP TABLE IF EXISTS stewarding_race_weekend; +DROP TABLE IF EXISTS stewarding_series; +DROP TABLE IF EXISTS stewarding_penalty_definition; +DROP TABLE IF EXISTS stewarding_penalty_catalog; +DROP TABLE IF EXISTS stewarding_reasoning_template; +DROP TABLE IF EXISTS stewarding_track; + +CREATE TABLE stewarding_track +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + country VARCHAR(100), + map_image_url VARCHAR(500), + map_metadata TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE stewarding_penalty_catalog +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE stewarding_penalty_definition +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + catalog_id VARCHAR(12) NOT NULL, + code VARCHAR(50), + name VARCHAR(255) NOT NULL, + description TEXT, + category VARCHAR(100), + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + default_penalty VARCHAR(255), + severity INTEGER, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (catalog_id) REFERENCES stewarding_penalty_catalog (id) +); + +CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON stewarding_penalty_definition (catalog_id); + +CREATE TABLE stewarding_reasoning_template +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE stewarding_series +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id VARCHAR(12), + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) +); + +CREATE TABLE stewarding_round +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + series_id VARCHAR(12), + track_id VARCHAR(12), + title VARCHAR(255) NOT NULL, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (series_id) REFERENCES stewarding_series (id), + FOREIGN KEY (track_id) REFERENCES stewarding_track (id) +); + +CREATE INDEX ix_stewarding_round_series_id ON stewarding_round (series_id); +CREATE INDEX ix_stewarding_round_track_id ON stewarding_round (track_id); + +CREATE TABLE stewarding_session +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12) NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (round_id) REFERENCES stewarding_round (id) +); + +CREATE INDEX ix_stewarding_session_round_id ON stewarding_session (round_id); + +CREATE TABLE stewarding_entrylist +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12), + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (round_id) REFERENCES stewarding_round (id) +); + +CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); + +CREATE TABLE stewarding_entrylist_entry +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + entrylist_id VARCHAR(12) NOT NULL, + race_number INTEGER NOT NULL, + car_model_id INTEGER, + team_name VARCHAR(255), + display_name VARCHAR(255), + FOREIGN KEY (entrylist_id) REFERENCES stewarding_entrylist (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON stewarding_entrylist_entry (entrylist_id); + +CREATE TABLE stewarding_entrylist_driver +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + entry_id VARCHAR(12) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + short_name VARCHAR(10), + steam_id VARCHAR(50), + category INTEGER, + FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON stewarding_entrylist_driver (entry_id); + +CREATE TABLE stewarding_incident +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + session_id VARCHAR(12) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + lap INTEGER, + timestamp_in_session VARCHAR(100), + map_marker_x REAL, + map_marker_y REAL, + video_url VARCHAR(500), + involved_cars_text VARCHAR(500), + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + reported_by_user_id INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES stewarding_session (id) +); + +CREATE INDEX ix_stewarding_incident_session_id ON stewarding_incident (session_id); +CREATE INDEX ix_stewarding_incident_status ON stewarding_incident (status); + +CREATE TABLE stewarding_incident_involved_entry +( + incident_id VARCHAR(12) NOT NULL, + entry_id VARCHAR(12) NOT NULL, + PRIMARY KEY (incident_id, entry_id), + FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id) ON DELETE CASCADE, + FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE +); + +CREATE TABLE stewarding_decision +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + incident_id VARCHAR(12), + session_id VARCHAR(12) NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id VARCHAR(12), + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id VARCHAR(12), + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id VARCHAR(12), + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id VARCHAR(12), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id), + FOREIGN KEY (session_id) REFERENCES stewarding_session (id), + FOREIGN KEY (penalty_definition_id) REFERENCES stewarding_penalty_definition (id), + FOREIGN KEY (reasoning_template_id) REFERENCES stewarding_reasoning_template (id), + FOREIGN KEY (penalized_entry_id) REFERENCES stewarding_entrylist_entry (id), + FOREIGN KEY (superseded_by_id) REFERENCES stewarding_decision (id) +); + +CREATE INDEX ix_stewarding_decision_incident_id ON stewarding_decision (incident_id); +CREATE INDEX ix_stewarding_decision_session_id ON stewarding_decision (session_id); +CREATE INDEX ix_stewarding_decision_is_active ON stewarding_decision (is_active); + +CREATE TABLE stewarding_appeal +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + decision_id VARCHAR(12) NOT NULL, + filed_by_user_id INTEGER, + filed_by_entry_id VARCHAR(12), + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + response TEXT, + responded_by_user_id INTEGER, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + responded_at TIMESTAMP, + FOREIGN KEY (decision_id) REFERENCES stewarding_decision (id), + FOREIGN KEY (filed_by_entry_id) REFERENCES stewarding_entrylist_entry (id) +); + +CREATE INDEX ix_stewarding_appeal_decision_id ON stewarding_appeal (decision_id); +CREATE INDEX ix_stewarding_appeal_status ON stewarding_appeal (status); From ea50a7802030ce7bf06f8b5e1e5fa89b97950aba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:37:09 +0000 Subject: [PATCH 19/24] refactor: change stewarding entity id and FK fields from Integer to String Update all stewarding entity classes to use String instead of Integer for primary key (id) and foreign key fields. User-related FK fields (decidedByUserId, filedByUserId, respondedByUserId, reportedByUserId) and non-stewarding FKs (carModelId) remain as Integer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../simdesk/entities/stewarding/Appeal.java | 6 +++--- .../simdesk/entities/stewarding/Incident.java | 4 ++-- .../entities/stewarding/PenaltyCatalog.java | 2 +- .../entities/stewarding/PenaltyDefinition.java | 4 ++-- .../entities/stewarding/ReasoningTemplate.java | 2 +- .../simdesk/entities/stewarding/Round.java | 6 +++--- .../simdesk/entities/stewarding/RoundSession.java | 4 ++-- .../simdesk/entities/stewarding/Series.java | 4 ++-- .../entities/stewarding/StewardDecision.java | 14 +++++++------- .../entities/stewarding/StewardingEntrylist.java | 4 ++-- .../stewarding/StewardingEntrylistDriver.java | 4 ++-- .../stewarding/StewardingEntrylistEntry.java | 4 ++-- .../entities/stewarding/StewardingTrack.java | 2 +- 13 files changed, 30 insertions(+), 30 deletions(-) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java index 77a31a76..81de0de4 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Appeal.java @@ -13,10 +13,10 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Appeal { - private Integer id; - private Integer decisionId; + private String id; + private String decisionId; private Integer filedByUserId; - private Integer filedByEntryId; + private String filedByEntryId; private String reason; private AppealStatus status; private String response; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java index 5093b3eb..9ce3d7c5 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Incident.java @@ -13,8 +13,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Incident { - private Integer id; - private Integer sessionId; + private String id; + private String sessionId; private String title; private String description; private Integer lap; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java index 9f7f0452..27b539a1 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyCatalog.java @@ -13,7 +13,7 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class PenaltyCatalog { - private Integer id; + private String id; private String name; private String description; private Instant createdAt; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java index 439d2f44..eaae4d1f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/PenaltyDefinition.java @@ -11,8 +11,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class PenaltyDefinition { - private Integer id; - private Integer catalogId; + private String id; + private String catalogId; private String code; private String name; private String description; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java index 452a447b..96b69a33 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/ReasoningTemplate.java @@ -11,7 +11,7 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class ReasoningTemplate { - private Integer id; + private String id; private String name; private String category; private String templateText; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java index 94ad6fc5..18216eb4 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Round.java @@ -15,9 +15,9 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Round { - private Integer id; - private Integer seriesId; - private Integer trackId; + private String id; + private String seriesId; + private String trackId; private String title; private LocalDate startDate; private LocalDate endDate; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java index 556bc799..17264cb3 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/RoundSession.java @@ -13,8 +13,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class RoundSession { - private Integer id; - private Integer roundId; + private String id; + private String roundId; private StewSessionType sessionType; private String title; private Instant startTime; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java index d87f0a26..7acb2aeb 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/Series.java @@ -15,12 +15,12 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Series { - private Integer id; + private String id; private String title; private String description; private String discordWebhookUrl; private Boolean videoUrlEnabled; - private Integer penaltyCatalogId; + private String penaltyCatalogId; private LocalDate startDate; private LocalDate endDate; private Instant createdAt; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java index 7b404079..72bd988a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardDecision.java @@ -13,18 +13,18 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardDecision { - private Integer id; - private Integer incidentId; - private Integer sessionId; + private String id; + private String incidentId; + private String sessionId; private Integer decidedByUserId; - private Integer penaltyDefinitionId; + private String penaltyDefinitionId; private String customPenalty; private String reasoning; - private Integer reasoningTemplateId; + private String reasoningTemplateId; private Boolean isNoAction; - private Integer penalizedEntryId; + private String penalizedEntryId; private String penalizedCarText; private Instant decidedAt; - private Integer supersededById; + private String supersededById; private Boolean isActive; } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java index fb4f6c2f..d1d1ae9f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylist.java @@ -13,8 +13,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardingEntrylist { - private Integer id; - private Integer roundId; + private String id; + private String roundId; private Instant uploadedAt; private String rawJson; } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java index 2dc7c77a..3ff1508a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistDriver.java @@ -11,8 +11,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardingEntrylistDriver { - private Integer id; - private Integer entryId; + private String id; + private String entryId; private String firstName; private String lastName; private String shortName; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java index ecbd0d63..6a2bf9ac 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingEntrylistEntry.java @@ -11,8 +11,8 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardingEntrylistEntry { - private Integer id; - private Integer entrylistId; + private String id; + private String entrylistId; private Integer raceNumber; private Integer carModelId; private String teamName; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java index 2d8683a0..3ed18761 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/entities/stewarding/StewardingTrack.java @@ -13,7 +13,7 @@ @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) public class StewardingTrack { - private Integer id; + private String id; private String name; private String country; private String mapImageUrl; From 6528bfb19cd8784a54c93fce5f9e46e61d2cd535 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:45:57 +0000 Subject: [PATCH 20/24] Update stewarding mapper interfaces: rename tables, use String IDs, explicit ID in INSERT - Use stewarding_round instead of stewarding_race_weekend - Use round_id instead of race_weekend_id - Remove @Options(useGeneratedKeys) from INSERT methods - Add id to INSERT column list and #{id} to VALUES - Change all ID parameters from Integer to String - Update all service and view callers accordingly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../mybatis/mapper/PenaltyCatalogMapper.java | 9 +++---- .../mapper/PenaltyDefinitionMapper.java | 15 ++++++----- .../mapper/ReasoningTemplateMapper.java | 9 +++---- .../simdesk/mybatis/mapper/RoundMapper.java | 25 +++++++++---------- .../mybatis/mapper/RoundSessionMapper.java | 17 ++++++------- .../simdesk/mybatis/mapper/SeriesMapper.java | 9 +++---- .../mybatis/mapper/StewardDecisionMapper.java | 19 +++++++------- .../mapper/StewardingAppealMapper.java | 11 ++++---- .../StewardingEntrylistDriverMapper.java | 11 ++++---- .../StewardingEntrylistEntryMapper.java | 11 ++++---- .../mapper/StewardingEntrylistMapper.java | 11 ++++---- ...StewardingIncidentInvolvedEntryMapper.java | 6 ++--- .../mapper/StewardingIncidentMapper.java | 13 +++++----- .../mybatis/mapper/StewardingTrackMapper.java | 9 +++---- .../stewarding/PenaltyCatalogService.java | 10 ++++---- .../stewarding/ReasoningTemplateService.java | 4 +-- .../services/stewarding/RoundService.java | 12 ++++----- .../services/stewarding/SeriesService.java | 4 +-- .../stewarding/StewardDecisionService.java | 12 ++++----- .../stewarding/StewardingAppealService.java | 6 ++--- .../StewardingDiscordNotificationService.java | 12 ++++----- .../StewardingEntrylistService.java | 10 ++++---- .../stewarding/StewardingIncidentService.java | 14 +++++------ .../stewarding/StewardingTrackService.java | 4 +-- .../views/stewarding/IncidentDetailView.java | 20 +++++---------- .../stewarding/PenaltyCatalogDetailView.java | 10 ++------ .../views/stewarding/RoundDetailView.java | 25 +++++++------------ .../views/stewarding/SeriesDetailView.java | 12 +++------ .../stewarding/PenaltyCatalogServiceTest.java | 10 ++++---- .../ReasoningTemplateServiceTest.java | 4 +-- .../StewardingEntrylistServiceTest.java | 12 ++++----- 31 files changed, 158 insertions(+), 198 deletions(-) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java index 9de3c234..7213688d 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyCatalogMapper.java @@ -21,13 +21,12 @@ public interface PenaltyCatalogMapper { @ResultMap("penaltyCatalogResultMap") @Select("SELECT * FROM stewarding_penalty_catalog WHERE id = #{id}") - PenaltyCatalog findById(Integer id); + PenaltyCatalog findById(String id); @Insert(""" - INSERT INTO stewarding_penalty_catalog (name, description, created_at, updated_at) - VALUES (#{name}, #{description}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO stewarding_penalty_catalog (id, name, description, created_at, updated_at) + VALUES (#{id}, #{name}, #{description}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(PenaltyCatalog catalog); @Update(""" @@ -38,5 +37,5 @@ INSERT INTO stewarding_penalty_catalog (name, description, created_at, updated_a void update(PenaltyCatalog catalog); @Delete("DELETE FROM stewarding_penalty_catalog WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java index 08498a7d..bcbebdd4 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/PenaltyDefinitionMapper.java @@ -22,11 +22,11 @@ public interface PenaltyDefinitionMapper { @Result(property = "sortOrder", column = "sort_order"), }) @Select("SELECT * FROM stewarding_penalty_definition WHERE catalog_id = #{catalogId} ORDER BY sort_order, name") - List findByCatalogId(Integer catalogId); + List findByCatalogId(String catalogId); @ResultMap("penaltyDefinitionResultMap") @Select("SELECT * FROM stewarding_penalty_definition WHERE id = #{id}") - PenaltyDefinition findById(Integer id); + PenaltyDefinition findById(String id); @ResultMap("penaltyDefinitionResultMap") @Select(""" @@ -34,17 +34,16 @@ public interface PenaltyDefinitionMapper { WHERE catalog_id = #{catalogId} AND (session_type = #{sessionType} OR session_type = 'ALL') ORDER BY sort_order, name """) - List findByCatalogIdAndSessionType(Integer catalogId, String sessionType); + List findByCatalogIdAndSessionType(String catalogId, String sessionType); @ResultMap("penaltyDefinitionResultMap") @Select("SELECT * FROM stewarding_penalty_definition WHERE catalog_id = #{catalogId} ORDER BY category, sort_order") - List findByCatalogIdGroupedByCategory(Integer catalogId); + List findByCatalogIdGroupedByCategory(String catalogId); @Insert(""" - INSERT INTO stewarding_penalty_definition (catalog_id, code, name, description, category, session_type, default_penalty, severity, sort_order) - VALUES (#{catalogId}, #{code}, #{name}, #{description}, #{category}, #{sessionType}, #{defaultPenalty}, #{severity}, #{sortOrder}) + INSERT INTO stewarding_penalty_definition (id, catalog_id, code, name, description, category, session_type, default_penalty, severity, sort_order) + VALUES (#{id}, #{catalogId}, #{code}, #{name}, #{description}, #{category}, #{sessionType}, #{defaultPenalty}, #{severity}, #{sortOrder}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(PenaltyDefinition definition); @Update(""" @@ -56,5 +55,5 @@ INSERT INTO stewarding_penalty_definition (catalog_id, code, name, description, void update(PenaltyDefinition definition); @Delete("DELETE FROM stewarding_penalty_definition WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java index 3d8da233..0bec7eb5 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/ReasoningTemplateMapper.java @@ -21,17 +21,16 @@ public interface ReasoningTemplateMapper { @ResultMap("reasoningTemplateResultMap") @Select("SELECT * FROM stewarding_reasoning_template WHERE id = #{id}") - ReasoningTemplate findById(Integer id); + ReasoningTemplate findById(String id); @ResultMap("reasoningTemplateResultMap") @Select("SELECT * FROM stewarding_reasoning_template WHERE category = #{category} ORDER BY sort_order") List findByCategory(String category); @Insert(""" - INSERT INTO stewarding_reasoning_template (name, category, template_text, sort_order) - VALUES (#{name}, #{category}, #{templateText}, #{sortOrder}) + INSERT INTO stewarding_reasoning_template (id, name, category, template_text, sort_order) + VALUES (#{id}, #{name}, #{category}, #{templateText}, #{sortOrder}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(ReasoningTemplate template); @Update(""" @@ -42,5 +41,5 @@ INSERT INTO stewarding_reasoning_template (name, category, template_text, sort_o void update(ReasoningTemplate template); @Delete("DELETE FROM stewarding_reasoning_template WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java index 99fee5c1..2740ab6c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundMapper.java @@ -19,36 +19,35 @@ public interface RoundMapper { @Result(property = "createdAt", column = "created_at"), @Result(property = "updatedAt", column = "updated_at"), }) - @Select("SELECT * FROM stewarding_race_weekend ORDER BY start_date DESC") + @Select("SELECT * FROM stewarding_round ORDER BY start_date DESC") List findAll(); @ResultMap("roundResultMap") - @Select("SELECT * FROM stewarding_race_weekend WHERE id = #{id}") - Round findById(Integer id); + @Select("SELECT * FROM stewarding_round WHERE id = #{id}") + Round findById(String id); @ResultMap("roundResultMap") - @Select("SELECT * FROM stewarding_race_weekend WHERE series_id = #{seriesId} ORDER BY start_date") - List findBySeriesId(Integer seriesId); + @Select("SELECT * FROM stewarding_round WHERE series_id = #{seriesId} ORDER BY start_date") + List findBySeriesId(String seriesId); @ResultMap("roundResultMap") - @Select("SELECT * FROM stewarding_race_weekend WHERE track_id = #{trackId}") - List findByTrackId(Integer trackId); + @Select("SELECT * FROM stewarding_round WHERE track_id = #{trackId}") + List findByTrackId(String trackId); @Insert(""" - INSERT INTO stewarding_race_weekend (series_id, track_id, title, start_date, end_date, created_at, updated_at) - VALUES (#{seriesId}, #{trackId}, #{title}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO stewarding_round (id, series_id, track_id, title, start_date, end_date, created_at, updated_at) + VALUES (#{id}, #{seriesId}, #{trackId}, #{title}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(Round round); @Update(""" - UPDATE stewarding_race_weekend + UPDATE stewarding_round SET series_id = #{seriesId}, track_id = #{trackId}, title = #{title}, start_date = #{startDate}, end_date = #{endDate}, updated_at = CURRENT_TIMESTAMP WHERE id = #{id} """) void update(Round round); - @Delete("DELETE FROM stewarding_race_weekend WHERE id = #{id}") - void delete(Integer id); + @Delete("DELETE FROM stewarding_round WHERE id = #{id}") + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java index f0cdf582..88e38049 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/RoundSessionMapper.java @@ -11,7 +11,7 @@ public interface RoundSessionMapper { @Results(id = "roundSessionResultMap", value = { @Result(id = true, property = "id", column = "id"), - @Result(property = "roundId", column = "race_weekend_id"), + @Result(property = "roundId", column = "round_id"), @Result(property = "sessionType", column = "session_type"), @Result(property = "title", column = "title"), @Result(property = "startTime", column = "start_time"), @@ -19,28 +19,27 @@ public interface RoundSessionMapper { @Result(property = "sortOrder", column = "sort_order"), @Result(property = "createdAt", column = "created_at"), }) - @Select("SELECT * FROM stewarding_session WHERE race_weekend_id = #{roundId} ORDER BY sort_order") - List findByRoundId(Integer roundId); + @Select("SELECT * FROM stewarding_session WHERE round_id = #{roundId} ORDER BY sort_order") + List findByRoundId(String roundId); @ResultMap("roundSessionResultMap") @Select("SELECT * FROM stewarding_session WHERE id = #{id}") - RoundSession findById(Integer id); + RoundSession findById(String id); @Insert(""" - INSERT INTO stewarding_session (race_weekend_id, session_type, title, start_time, end_time, sort_order, created_at) - VALUES (#{roundId}, #{sessionType}, #{title}, #{startTime}, #{endTime}, #{sortOrder}, CURRENT_TIMESTAMP) + INSERT INTO stewarding_session (id, round_id, session_type, title, start_time, end_time, sort_order, created_at) + VALUES (#{id}, #{roundId}, #{sessionType}, #{title}, #{startTime}, #{endTime}, #{sortOrder}, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(RoundSession session); @Update(""" UPDATE stewarding_session - SET race_weekend_id = #{roundId}, session_type = #{sessionType}, title = #{title}, + SET round_id = #{roundId}, session_type = #{sessionType}, title = #{title}, start_time = #{startTime}, end_time = #{endTime}, sort_order = #{sortOrder} WHERE id = #{id} """) void update(RoundSession session); @Delete("DELETE FROM stewarding_session WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java index 8a0e8dc8..db0ce24a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/SeriesMapper.java @@ -26,13 +26,12 @@ public interface SeriesMapper { @ResultMap("seriesResultMap") @Select("SELECT * FROM stewarding_series WHERE id = #{id}") - Series findById(Integer id); + Series findById(String id); @Insert(""" - INSERT INTO stewarding_series (title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at) - VALUES (#{title}, #{description}, #{discordWebhookUrl}, #{videoUrlEnabled}, #{penaltyCatalogId}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO stewarding_series (id, title, description, discord_webhook_url, video_url_enabled, penalty_catalog_id, start_date, end_date, created_at, updated_at) + VALUES (#{id}, #{title}, #{description}, #{discordWebhookUrl}, #{videoUrlEnabled}, #{penaltyCatalogId}, #{startDate}, #{endDate}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(Series series); @Update(""" @@ -45,5 +44,5 @@ INSERT INTO stewarding_series (title, description, discord_webhook_url, video_ur void update(Series series); @Delete("DELETE FROM stewarding_series WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java index c02da68e..98e32883 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardDecisionMapper.java @@ -26,36 +26,35 @@ public interface StewardDecisionMapper { @Result(property = "isActive", column = "is_active"), }) @Select("SELECT * FROM stewarding_decision WHERE id = #{id}") - StewardDecision findById(Integer id); + StewardDecision findById(String id); @ResultMap("stewardDecisionResultMap") @Select("SELECT * FROM stewarding_decision WHERE incident_id = #{incidentId} AND is_active = true") - List findActiveByIncidentId(Integer incidentId); + List findActiveByIncidentId(String incidentId); @ResultMap("stewardDecisionResultMap") @Select("SELECT * FROM stewarding_decision WHERE incident_id = #{incidentId} ORDER BY decided_at DESC") - List findByIncidentId(Integer incidentId); + List findByIncidentId(String incidentId); @ResultMap("stewardDecisionResultMap") @Select("SELECT * FROM stewarding_decision WHERE session_id = #{sessionId} AND is_active = true ORDER BY decided_at DESC") - List findBySessionId(Integer sessionId); + List findBySessionId(String sessionId); @ResultMap("stewardDecisionResultMap") @Select("SELECT * FROM stewarding_decision WHERE incident_id IS NULL AND session_id = #{sessionId} AND is_active = true ORDER BY decided_at DESC") - List findManualBySessionId(Integer sessionId); + List findManualBySessionId(String sessionId); @Insert(""" - INSERT INTO stewarding_decision (incident_id, session_id, decided_by_user_id, penalty_definition_id, custom_penalty, + INSERT INTO stewarding_decision (id, incident_id, session_id, decided_by_user_id, penalty_definition_id, custom_penalty, reasoning, reasoning_template_id, is_no_action, penalized_entry_id, penalized_car_text, decided_at, superseded_by_id, is_active) - VALUES (#{incidentId}, #{sessionId}, #{decidedByUserId}, #{penaltyDefinitionId}, #{customPenalty}, + VALUES (#{id}, #{incidentId}, #{sessionId}, #{decidedByUserId}, #{penaltyDefinitionId}, #{customPenalty}, #{reasoning}, #{reasoningTemplateId}, #{isNoAction}, #{penalizedEntryId}, #{penalizedCarText}, CURRENT_TIMESTAMP, #{supersededById}, #{isActive}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardDecision decision); @Update("UPDATE stewarding_decision SET is_active = false WHERE id = #{id}") - void deactivate(Integer id); + void deactivate(String id); @Update("UPDATE stewarding_decision SET superseded_by_id = #{supersededById} WHERE id = #{id}") - void setSupersededBy(Integer id, Integer supersededById); + void setSupersededBy(String id, String supersededById); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java index b83e5d9f..2cde2ba6 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingAppealMapper.java @@ -22,17 +22,16 @@ public interface StewardingAppealMapper { @Result(property = "respondedAt", column = "responded_at"), }) @Select("SELECT * FROM stewarding_appeal WHERE decision_id = #{decisionId} ORDER BY filed_at DESC") - List findByDecisionId(Integer decisionId); + List findByDecisionId(String decisionId); @ResultMap("stewardingAppealResultMap") @Select("SELECT * FROM stewarding_appeal WHERE id = #{id}") - Appeal findById(Integer id); + Appeal findById(String id); @Insert(""" - INSERT INTO stewarding_appeal (decision_id, filed_by_user_id, filed_by_entry_id, reason, status, filed_at) - VALUES (#{decisionId}, #{filedByUserId}, #{filedByEntryId}, #{reason}, #{status}, CURRENT_TIMESTAMP) + INSERT INTO stewarding_appeal (id, decision_id, filed_by_user_id, filed_by_entry_id, reason, status, filed_at) + VALUES (#{id}, #{decisionId}, #{filedByUserId}, #{filedByEntryId}, #{reason}, #{status}, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(Appeal appeal); @Update(""" @@ -40,5 +39,5 @@ INSERT INTO stewarding_appeal (decision_id, filed_by_user_id, filed_by_entry_id, SET status = #{status}, response = #{response}, responded_by_user_id = #{respondedByUserId}, responded_at = CURRENT_TIMESTAMP WHERE id = #{id} """) - void updateResponse(Integer id, String status, String response, Integer respondedByUserId); + void updateResponse(String id, String status, String response, Integer respondedByUserId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java index 891e9480..ce5ba519 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistDriverMapper.java @@ -19,19 +19,18 @@ public interface StewardingEntrylistDriverMapper { @Result(property = "category", column = "category"), }) @Select("SELECT * FROM stewarding_entrylist_driver WHERE entry_id = #{entryId}") - List findByEntryId(Integer entryId); + List findByEntryId(String entryId); @ResultMap("stewardingEntrylistDriverResultMap") @Select("SELECT * FROM stewarding_entrylist_driver WHERE id = #{id}") - StewardingEntrylistDriver findById(Integer id); + StewardingEntrylistDriver findById(String id); @Insert(""" - INSERT INTO stewarding_entrylist_driver (entry_id, first_name, last_name, short_name, steam_id, category) - VALUES (#{entryId}, #{firstName}, #{lastName}, #{shortName}, #{steamId}, #{category}) + INSERT INTO stewarding_entrylist_driver (id, entry_id, first_name, last_name, short_name, steam_id, category) + VALUES (#{id}, #{entryId}, #{firstName}, #{lastName}, #{shortName}, #{steamId}, #{category}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingEntrylistDriver driver); @Delete("DELETE FROM stewarding_entrylist_driver WHERE entry_id = #{entryId}") - void deleteByEntryId(Integer entryId); + void deleteByEntryId(String entryId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java index 2cd6f83a..c833c22c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistEntryMapper.java @@ -18,19 +18,18 @@ public interface StewardingEntrylistEntryMapper { @Result(property = "displayName", column = "display_name"), }) @Select("SELECT * FROM stewarding_entrylist_entry WHERE entrylist_id = #{entrylistId} ORDER BY race_number") - List findByEntrylistId(Integer entrylistId); + List findByEntrylistId(String entrylistId); @ResultMap("stewardingEntrylistEntryResultMap") @Select("SELECT * FROM stewarding_entrylist_entry WHERE id = #{id}") - StewardingEntrylistEntry findById(Integer id); + StewardingEntrylistEntry findById(String id); @Insert(""" - INSERT INTO stewarding_entrylist_entry (entrylist_id, race_number, car_model_id, team_name, display_name) - VALUES (#{entrylistId}, #{raceNumber}, #{carModelId}, #{teamName}, #{displayName}) + INSERT INTO stewarding_entrylist_entry (id, entrylist_id, race_number, car_model_id, team_name, display_name) + VALUES (#{id}, #{entrylistId}, #{raceNumber}, #{carModelId}, #{teamName}, #{displayName}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingEntrylistEntry entry); @Delete("DELETE FROM stewarding_entrylist_entry WHERE entrylist_id = #{entrylistId}") - void deleteByEntrylistId(Integer entrylistId); + void deleteByEntrylistId(String entrylistId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java index 5777df8c..33bb5732 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingEntrylistMapper.java @@ -16,19 +16,18 @@ public interface StewardingEntrylistMapper { @Result(property = "rawJson", column = "raw_json"), }) @Select("SELECT * FROM stewarding_entrylist WHERE round_id = #{roundId}") - List findByRoundId(Integer roundId); + List findByRoundId(String roundId); @ResultMap("stewardingEntrylistResultMap") @Select("SELECT * FROM stewarding_entrylist WHERE id = #{id}") - StewardingEntrylist findById(Integer id); + StewardingEntrylist findById(String id); @Insert(""" - INSERT INTO stewarding_entrylist (round_id, uploaded_at, raw_json) - VALUES (#{roundId}, CURRENT_TIMESTAMP, #{rawJson}) + INSERT INTO stewarding_entrylist (id, round_id, uploaded_at, raw_json) + VALUES (#{id}, #{roundId}, CURRENT_TIMESTAMP, #{rawJson}) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingEntrylist entrylist); @Delete("DELETE FROM stewarding_entrylist WHERE round_id = #{roundId}") - void deleteByRoundId(Integer roundId); + void deleteByRoundId(String roundId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java index 70aaf586..fd0a9a1e 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentInvolvedEntryMapper.java @@ -12,11 +12,11 @@ @Mapper public interface StewardingIncidentInvolvedEntryMapper { @Select("SELECT entry_id FROM stewarding_incident_involved_entry WHERE incident_id = #{incidentId}") - List findEntryIdsByIncidentId(Integer incidentId); + List findEntryIdsByIncidentId(String incidentId); @Insert("INSERT INTO stewarding_incident_involved_entry (incident_id, entry_id) VALUES (#{incidentId}, #{entryId})") - void insert(Integer incidentId, Integer entryId); + void insert(String incidentId, String entryId); @Delete("DELETE FROM stewarding_incident_involved_entry WHERE incident_id = #{incidentId}") - void deleteByIncidentId(Integer incidentId); + void deleteByIncidentId(String incidentId); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java index 1d589adb..be252fe0 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingIncidentMapper.java @@ -26,27 +26,26 @@ public interface StewardingIncidentMapper { @Result(property = "updatedAt", column = "updated_at"), }) @Select("SELECT * FROM stewarding_incident WHERE session_id = #{sessionId} ORDER BY created_at DESC") - List findBySessionId(Integer sessionId); + List findBySessionId(String sessionId); @ResultMap("stewardingIncidentResultMap") @Select("SELECT * FROM stewarding_incident WHERE id = #{id}") - Incident findById(Integer id); + Incident findById(String id); @ResultMap("stewardingIncidentResultMap") @Select("SELECT * FROM stewarding_incident WHERE session_id = #{sessionId} AND status = #{status} ORDER BY created_at DESC") - List findBySessionIdAndStatus(Integer sessionId, String status); + List findBySessionIdAndStatus(String sessionId, String status); @Insert(""" - INSERT INTO stewarding_incident (session_id, title, description, lap, timestamp_in_session, map_marker_x, map_marker_y, + INSERT INTO stewarding_incident (id, session_id, title, description, lap, timestamp_in_session, map_marker_x, map_marker_y, video_url, involved_cars_text, status, reported_by_user_id, created_at, updated_at) - VALUES (#{sessionId}, #{title}, #{description}, #{lap}, #{timestampInSession}, #{mapMarkerX}, #{mapMarkerY}, + VALUES (#{id}, #{sessionId}, #{title}, #{description}, #{lap}, #{timestampInSession}, #{mapMarkerX}, #{mapMarkerY}, #{videoUrl}, #{involvedCarsText}, #{status}, #{reportedByUserId}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(Incident incident); @Update("UPDATE stewarding_incident SET status = #{status}, updated_at = CURRENT_TIMESTAMP WHERE id = #{id}") - void updateStatus(Integer id, String status); + void updateStatus(String id, String status); @Update(""" UPDATE stewarding_incident diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java index 36847127..a90fe766 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/mybatis/mapper/StewardingTrackMapper.java @@ -23,13 +23,12 @@ public interface StewardingTrackMapper { @ResultMap("stewardingTrackResultMap") @Select("SELECT * FROM stewarding_track WHERE id = #{id}") - StewardingTrack findById(Integer id); + StewardingTrack findById(String id); @Insert(""" - INSERT INTO stewarding_track (name, country, map_image_url, map_metadata, created_at, updated_at) - VALUES (#{name}, #{country}, #{mapImageUrl}, #{mapMetadata}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO stewarding_track (id, name, country, map_image_url, map_metadata, created_at, updated_at) + VALUES (#{id}, #{name}, #{country}, #{mapImageUrl}, #{mapMetadata}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """) - @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") void insert(StewardingTrack track); @Update(""" @@ -40,5 +39,5 @@ INSERT INTO stewarding_track (name, country, map_image_url, map_metadata, create void update(StewardingTrack track); @Delete("DELETE FROM stewarding_track WHERE id = #{id}") - void delete(Integer id); + void delete(String id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java index b48cad05..984c739c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java @@ -23,7 +23,7 @@ public List getAllCatalogs() { return catalogMapper.findAll(); } - public PenaltyCatalog getCatalogById(Integer id) { + public PenaltyCatalog getCatalogById(String id) { return catalogMapper.findById(id); } @@ -38,15 +38,15 @@ public void updateCatalog(PenaltyCatalog catalog) { } @Transactional - public void deleteCatalog(Integer id) { + public void deleteCatalog(String id) { catalogMapper.delete(id); } - public List getDefinitionsByCatalogId(Integer catalogId) { + public List getDefinitionsByCatalogId(String catalogId) { return definitionMapper.findByCatalogId(catalogId); } - public List getDefinitionsForSessionType(Integer catalogId, String sessionType) { + public List getDefinitionsForSessionType(String catalogId, String sessionType) { return definitionMapper.findByCatalogIdAndSessionType(catalogId, sessionType); } @@ -61,7 +61,7 @@ public void updateDefinition(PenaltyDefinition definition) { } @Transactional - public void deleteDefinition(Integer id) { + public void deleteDefinition(String id) { definitionMapper.delete(id); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java index 1718b3fa..93834f1a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java @@ -21,7 +21,7 @@ public List getAllTemplates() { return templateMapper.findAll(); } - public ReasoningTemplate getTemplateById(Integer id) { + public ReasoningTemplate getTemplateById(String id) { return templateMapper.findById(id); } @@ -40,7 +40,7 @@ public void updateTemplate(ReasoningTemplate template) { } @Transactional - public void deleteTemplate(Integer id) { + public void deleteTemplate(String id) { templateMapper.delete(id); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java index e6e475e0..db3198a1 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java @@ -25,7 +25,7 @@ public List getAllRounds() { return roundMapper.findAll(); } - public Round getRoundById(Integer id) { + public Round getRoundById(String id) { Round round = roundMapper.findById(id); if (round != null && round.getTrackId() != null) { round.setTrack(trackMapper.findById(round.getTrackId())); @@ -33,7 +33,7 @@ public Round getRoundById(Integer id) { return round; } - public List getRoundsBySeriesId(Integer seriesId) { + public List getRoundsBySeriesId(String seriesId) { return roundMapper.findBySeriesId(seriesId); } @@ -48,15 +48,15 @@ public void updateRound(Round round) { } @Transactional - public void deleteRound(Integer id) { + public void deleteRound(String id) { roundMapper.delete(id); } - public List getSessionsByRoundId(Integer roundId) { + public List getSessionsByRoundId(String roundId) { return sessionMapper.findByRoundId(roundId); } - public RoundSession getSessionById(Integer id) { + public RoundSession getSessionById(String id) { return sessionMapper.findById(id); } @@ -71,7 +71,7 @@ public void updateSession(RoundSession session) { } @Transactional - public void deleteSession(Integer id) { + public void deleteSession(String id) { sessionMapper.delete(id); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java index bee09847..cbc15a32 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java @@ -22,7 +22,7 @@ public List getAllSeries() { return seriesMapper.findAll(); } - public Series getSeriesById(Integer id) { + public Series getSeriesById(String id) { Series series = seriesMapper.findById(id); if (series != null && series.getPenaltyCatalogId() != null) { series.setPenaltyCatalog(catalogMapper.findById(series.getPenaltyCatalogId())); @@ -41,7 +41,7 @@ public void updateSeries(Series series) { } @Transactional - public void deleteSeries(Integer id) { + public void deleteSeries(String id) { seriesMapper.delete(id); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java index b93a3416..c7976ff1 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java @@ -19,24 +19,24 @@ public class StewardDecisionService { private final StewardDecisionMapper decisionMapper; private final StewardingIncidentMapper incidentMapper; - public StewardDecision getDecisionById(Integer id) { + public StewardDecision getDecisionById(String id) { return decisionMapper.findById(id); } - public StewardDecision getActiveDecisionByIncidentId(Integer incidentId) { + public StewardDecision getActiveDecisionByIncidentId(String incidentId) { List decisions = decisionMapper.findActiveByIncidentId(incidentId); return decisions.isEmpty() ? null : decisions.getFirst(); } - public List getDecisionHistory(Integer incidentId) { + public List getDecisionHistory(String incidentId) { return decisionMapper.findByIncidentId(incidentId); } - public List getDecisionsBySessionId(Integer sessionId) { + public List getDecisionsBySessionId(String sessionId) { return decisionMapper.findBySessionId(sessionId); } - public List getManualDecisionsBySessionId(Integer sessionId) { + public List getManualDecisionsBySessionId(String sessionId) { return decisionMapper.findManualBySessionId(sessionId); } @@ -49,7 +49,7 @@ public void makeDecision(StewardDecision decision) { } @Transactional - public void reviseDecision(Integer oldDecisionId, StewardDecision newDecision) { + public void reviseDecision(String oldDecisionId, StewardDecision newDecision) { decisionMapper.deactivate(oldDecisionId); decisionMapper.insert(newDecision); decisionMapper.setSupersededBy(oldDecisionId, newDecision.getId()); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java index 54338a94..b886234e 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java @@ -23,11 +23,11 @@ public class StewardingAppealService { private final StewardingIncidentMapper incidentMapper; private final StewardDecisionMapper decisionMapper; - public List getAppealsByDecisionId(Integer decisionId) { + public List getAppealsByDecisionId(String decisionId) { return appealMapper.findByDecisionId(decisionId); } - public Appeal getAppealById(Integer id) { + public Appeal getAppealById(String id) { return appealMapper.findById(id); } @@ -41,7 +41,7 @@ public void fileAppeal(Appeal appeal) { } @Transactional - public void reviewAppeal(Integer id, AppealStatus status, String response, Integer respondedByUserId) { + public void reviewAppeal(String id, AppealStatus status, String response, Integer respondedByUserId) { appealMapper.updateResponse(id, status.name(), response, respondedByUserId); if (status == AppealStatus.ACCEPTED || status == AppealStatus.REJECTED) { Appeal appeal = appealMapper.findById(id); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java index e9d10266..c6ca7a6c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingDiscordNotificationService.java @@ -34,7 +34,7 @@ public StewardingDiscordNotificationService(SeriesMapper seriesMapper) { } @Async - public void sendIncidentNotification(Integer seriesId, Incident incident) { + public void sendIncidentNotification(String seriesId, Incident incident) { String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; @@ -55,7 +55,7 @@ public void sendIncidentNotification(Integer seriesId, Incident incident) { } @Async - public void sendDecisionNotification(Integer seriesId, StewardDecision decision, Incident incident, String penaltyName) { + public void sendDecisionNotification(String seriesId, StewardDecision decision, Incident incident, String penaltyName) { String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; @@ -80,7 +80,7 @@ public void sendDecisionNotification(Integer seriesId, StewardDecision decision, } @Async - public void sendAppealNotification(Integer seriesId, Appeal appeal) { + public void sendAppealNotification(String seriesId, Appeal appeal) { String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; @@ -101,7 +101,7 @@ public void sendAppealNotification(Integer seriesId, Appeal appeal) { } @Async - public void sendAppealReviewedNotification(Integer seriesId, Appeal appeal) { + public void sendAppealReviewedNotification(String seriesId, Appeal appeal) { String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; @@ -124,7 +124,7 @@ public void sendAppealReviewedNotification(Integer seriesId, Appeal appeal) { } @Async - public void sendDecisionRevisedNotification(Integer seriesId, StewardDecision oldDecision, StewardDecision newDecision) { + public void sendDecisionRevisedNotification(String seriesId, StewardDecision oldDecision, StewardDecision newDecision) { String webhookUrl = getWebhookUrl(seriesId); if (webhookUrl == null) { return; @@ -145,7 +145,7 @@ public void sendDecisionRevisedNotification(Integer seriesId, StewardDecision ol sendWebhook(webhookUrl, Map.of("embeds", List.of(embed))); } - private String getWebhookUrl(Integer seriesId) { + private String getWebhookUrl(String seriesId) { Series series = seriesMapper.findById(seriesId); if (series == null || series.getDiscordWebhookUrl() == null || series.getDiscordWebhookUrl().isBlank()) { return null; diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java index d198e02f..8dcb9424 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java @@ -25,21 +25,21 @@ public class StewardingEntrylistService { private final StewardingEntrylistDriverMapper driverMapper; private final ObjectMapper objectMapper; - public StewardingEntrylist getEntrylistByRoundId(Integer roundId) { + public StewardingEntrylist getEntrylistByRoundId(String roundId) { List entrylists = entrylistMapper.findByRoundId(roundId); return entrylists.isEmpty() ? null : entrylists.getFirst(); } - public List getEntriesByEntrylistId(Integer entrylistId) { + public List getEntriesByEntrylistId(String entrylistId) { return entryMapper.findByEntrylistId(entrylistId); } - public List getDriversByEntryId(Integer entryId) { + public List getDriversByEntryId(String entryId) { return driverMapper.findByEntryId(entryId); } @Transactional - public void uploadEntrylistForRound(Integer roundId, String jsonContent) { + public void uploadEntrylistForRound(String roundId, String jsonContent) { deleteEntrylistForRound(roundId); JsonNode root; @@ -98,7 +98,7 @@ private void parseAndInsertEntries(JsonNode root, StewardingEntrylist entrylist) } @Transactional - public void deleteEntrylistForRound(Integer roundId) { + public void deleteEntrylistForRound(String roundId) { List existing = entrylistMapper.findByRoundId(roundId); for (StewardingEntrylist entrylist : existing) { List entries = entryMapper.findByEntrylistId(entrylist.getId()); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java index 5313138b..5d7630d3 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java @@ -19,32 +19,32 @@ public class StewardingIncidentService { private final StewardingIncidentMapper incidentMapper; private final StewardingIncidentInvolvedEntryMapper involvedEntryMapper; - public List getIncidentsBySessionId(Integer sessionId) { + public List getIncidentsBySessionId(String sessionId) { return incidentMapper.findBySessionId(sessionId); } - public Incident getIncidentById(Integer id) { + public Incident getIncidentById(String id) { return incidentMapper.findById(id); } - public List getIncidentsBySessionIdAndStatus(Integer sessionId, IncidentStatus status) { + public List getIncidentsBySessionIdAndStatus(String sessionId, IncidentStatus status) { return incidentMapper.findBySessionIdAndStatus(sessionId, status.name()); } @Transactional - public void createIncident(Incident incident, List involvedEntryIds) { + public void createIncident(Incident incident, List involvedEntryIds) { incidentMapper.insert(incident); - for (Integer entryId : involvedEntryIds) { + for (String entryId : involvedEntryIds) { involvedEntryMapper.insert(incident.getId(), entryId); } } @Transactional - public void updateIncidentStatus(Integer id, IncidentStatus status) { + public void updateIncidentStatus(String id, IncidentStatus status) { incidentMapper.updateStatus(id, status.name()); } - public List getInvolvedEntryIds(Integer incidentId) { + public List getInvolvedEntryIds(String incidentId) { return involvedEntryMapper.findEntryIdsByIncidentId(incidentId); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java index 84b55c2e..7e063890 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java @@ -20,7 +20,7 @@ public List getAllTracks() { return trackMapper.findAll(); } - public StewardingTrack getTrackById(Integer id) { + public StewardingTrack getTrackById(String id) { return trackMapper.findById(id); } @@ -35,7 +35,7 @@ public void updateTrack(StewardingTrack track) { } @Transactional - public void deleteTrack(Integer id) { + public void deleteTrack(String id) { trackMapper.delete(id); } } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index cd6c9ea7..b40b74f5 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -82,17 +82,9 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - Integer seriesId; - Integer roundId; - Integer incidentId; - try { - seriesId = Integer.valueOf(seriesIdParam); - roundId = Integer.valueOf(roundIdParam); - incidentId = Integer.valueOf(incidentIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); - return; - } + String seriesId = seriesIdParam; + String roundId = roundIdParam; + String incidentId = incidentIdParam; Series series = seriesService.getSeriesById(seriesId); Round round = roundService.getRoundById(roundId); @@ -185,7 +177,7 @@ private VerticalLayout createDecisionSection(Incident incident, Series series) { return layout; } - private FormLayout createDecisionForm(Incident incident, Series series, Integer existingDecisionId) { + private FormLayout createDecisionForm(Incident incident, Series series, String existingDecisionId) { FormLayout form = new FormLayout(); form.setResponsiveSteps( new FormLayout.ResponsiveStep("0", 1), @@ -253,7 +245,7 @@ private FormLayout createDecisionForm(Incident incident, Series series, Integer return form; } - private VerticalLayout createDecisionHistorySection(Integer incidentId) { + private VerticalLayout createDecisionHistorySection(String incidentId) { VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.add(new H3("Decision History")); @@ -280,7 +272,7 @@ private VerticalLayout createDecisionHistorySection(Integer incidentId) { return layout; } - private VerticalLayout createAppealsSection(Integer incidentId) { + private VerticalLayout createAppealsSection(String incidentId) { VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.add(new H3("Appeals")); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java index 6e19a5f2..6390fd6f 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java @@ -56,13 +56,7 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - Integer catalogId; - try { - catalogId = Integer.valueOf(catalogIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(PenaltyCatalogListView.class)); - return; - } + String catalogId = catalogIdParam; PenaltyCatalog catalog = catalogService.getCatalogById(catalogId); if (catalog == null) { @@ -109,7 +103,7 @@ public void beforeEnter(BeforeEnterEvent event) { addAndExpand(grid); } - private void openPenaltyDialog(Integer catalogId) { + private void openPenaltyDialog(String catalogId) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Add Penalty Definition"); dialog.setWidth("600px"); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java index 5d745efb..58fded44 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java @@ -90,15 +90,8 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - Integer seriesId; - Integer roundId; - try { - seriesId = Integer.valueOf(seriesIdParam); - roundId = Integer.valueOf(roundIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); - return; - } + String seriesId = seriesIdParam; + String roundId = roundIdParam; Series series = seriesService.getSeriesById(seriesId); Round round = roundService.getRoundById(roundId); @@ -173,7 +166,7 @@ private HorizontalLayout createDetailRow(String label, String value) { return row; } - private VerticalLayout createSessionsTab(Integer roundId) { + private VerticalLayout createSessionsTab(String roundId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); @@ -201,7 +194,7 @@ private VerticalLayout createSessionsTab(Integer roundId) { return layout; } - private void openAddSessionDialog(Integer roundId) { + private void openAddSessionDialog(String roundId) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Add Session"); dialog.setWidth("600px"); @@ -266,7 +259,7 @@ private void openAddSessionDialog(Integer roundId) { dialog.open(); } - private VerticalLayout createIncidentsTab(Integer seriesId, Integer roundId, Series series) { + private VerticalLayout createIncidentsTab(String seriesId, String roundId, Series series) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); @@ -310,7 +303,7 @@ private VerticalLayout createIncidentsTab(Integer seriesId, Integer roundId, Ser return layout; } - private void openReportIncidentDialog(Integer seriesId, Integer roundId, Series series) { + private void openReportIncidentDialog(String seriesId, String roundId, Series series) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Report Incident"); dialog.setWidth("600px"); @@ -382,7 +375,7 @@ private void openReportIncidentDialog(Integer seriesId, Integer roundId, Series String involvedCarsText = selectedEntries.stream() .map(StewardingEntrylistEntry::getDisplayName) .collect(Collectors.joining(", ")); - List involvedEntryIds = selectedEntries.stream() + List involvedEntryIds = selectedEntries.stream() .map(StewardingEntrylistEntry::getId) .collect(Collectors.toList()); @@ -414,7 +407,7 @@ private void openReportIncidentDialog(Integer seriesId, Integer roundId, Series dialog.open(); } - private VerticalLayout createEntrylistTab(Integer roundId) { + private VerticalLayout createEntrylistTab(String roundId) { VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); layout.setPadding(true); @@ -460,7 +453,7 @@ private VerticalLayout createEntrylistTab(Integer roundId) { return layout; } - private void openEditRoundDialog(Round round, Integer seriesId) { + private void openEditRoundDialog(Round round, String seriesId) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("Edit Round"); dialog.setWidth("600px"); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java index 1a0b4ff8..8f18aeda 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java @@ -77,13 +77,7 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - Integer seriesId; - try { - seriesId = Integer.valueOf(seriesIdParam); - } catch (NumberFormatException e) { - getUI().ifPresent(ui -> ui.navigate(SeriesListView.class)); - return; - } + String seriesId = seriesIdParam; Series series = seriesService.getSeriesById(seriesId); if (series == null) { @@ -267,7 +261,7 @@ private void openEditSeriesDialog(Series series) { dialog.open(); } - private void openNewRoundDialog(Integer seriesId) { + private void openNewRoundDialog(String seriesId) { Dialog dialog = new Dialog(); dialog.setHeaderTitle("New Round"); dialog.setWidth("600px"); @@ -305,7 +299,7 @@ private void openNewRoundDialog(Integer seriesId) { return; } - Round round = Round.builder() + Round round = Round.builder() .seriesId(seriesId) .title(titleField.getValue()) .trackId(trackCombo.getValue() != null ? trackCombo.getValue().getId() : null) diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java index 3c0b2e8e..4c3339bb 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java @@ -36,18 +36,18 @@ class PenaltyCatalogServiceTest { @Test void getDefinitionsForSessionType_shouldReturnMatchingDefinitions() { PenaltyDefinition racePenalty = PenaltyDefinition.builder() - .id(1) - .catalogId(1) + .id("1") + .catalogId("1") .code("PEN-001") .name("Causing a collision") .sessionType(PenaltySessionType.RACE) .defaultPenalty("5 second time penalty") .build(); - when(penaltyDefinitionMapper.findByCatalogIdAndSessionType(1, "RACE")) + when(penaltyDefinitionMapper.findByCatalogIdAndSessionType("1", "RACE")) .thenReturn(List.of(racePenalty)); - List result = penaltyCatalogService.getDefinitionsForSessionType(1, "RACE"); + List result = penaltyCatalogService.getDefinitionsForSessionType("1", "RACE"); assertThat(result).hasSize(1); assertThat(result.getFirst().getName()).isEqualTo("Causing a collision"); @@ -56,7 +56,7 @@ void getDefinitionsForSessionType_shouldReturnMatchingDefinitions() { @Test void getAllCatalogs_shouldReturnAll() { PenaltyCatalog catalog = PenaltyCatalog.builder() - .id(1) + .id("1") .name("2025 Season Rules") .description("Standard penalty rules for 2025") .build(); diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java index 95b535f9..16ac8805 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java @@ -65,7 +65,7 @@ void renderTemplate_shouldHandleEmptyVariables() { @Test void getAllTemplates_shouldReturnAll() { ReasoningTemplate template = ReasoningTemplate.builder() - .id(1) + .id("1") .name("Test Template") .category("Contact") .templateText("Template text") @@ -83,7 +83,7 @@ void getAllTemplates_shouldReturnAll() { @Test void getTemplatesByCategory_shouldFilterByCategory() { ReasoningTemplate template = ReasoningTemplate.builder() - .id(1) + .id("1") .name("Contact Template") .category("Contact") .templateText("Contact template text") diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java index 0c57d933..6c491a09 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java @@ -41,7 +41,7 @@ class StewardingEntrylistServiceTest { @Test void uploadEntrylistForRound_shouldParseValidAccJson() { - when(entrylistMapper.findByRoundId(1)).thenReturn(Collections.emptyList()); + when(entrylistMapper.findByRoundId("1")).thenReturn(Collections.emptyList()); String accJson = """ { @@ -86,9 +86,9 @@ void uploadEntrylistForRound_shouldParseValidAccJson() { } """; - entrylistService.uploadEntrylistForRound(1, accJson); + entrylistService.uploadEntrylistForRound("1", accJson); - verify(entrylistMapper).deleteByRoundId(1); + verify(entrylistMapper).deleteByRoundId("1"); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, times(2)).insert(any(StewardingEntrylistEntry.class)); verify(entrylistDriverMapper, times(3)).insert(any(StewardingEntrylistDriver.class)); @@ -96,7 +96,7 @@ void uploadEntrylistForRound_shouldParseValidAccJson() { @Test void uploadEntrylistForRound_shouldHandleEmptyEntries() { - when(entrylistMapper.findByRoundId(1)).thenReturn(Collections.emptyList()); + when(entrylistMapper.findByRoundId("1")).thenReturn(Collections.emptyList()); String accJson = """ { @@ -105,9 +105,9 @@ void uploadEntrylistForRound_shouldHandleEmptyEntries() { } """; - entrylistService.uploadEntrylistForRound(1, accJson); + entrylistService.uploadEntrylistForRound("1", accJson); - verify(entrylistMapper).deleteByRoundId(1); + verify(entrylistMapper).deleteByRoundId("1"); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, never()).insert(any()); verify(entrylistDriverMapper, never()).insert(any()); From be717009075585cea7a31fb6e357b8825dcc7d4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:17 +0000 Subject: [PATCH 21/24] Inject IdGenerator into all stewarding service classes for ID generation Add IdGenerator dependency to all 9 stewarding service classes and call idGenerator.generateRandomString(12) in their create methods before inserting entities into the database. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../simdesk/services/stewarding/PenaltyCatalogService.java | 4 ++++ .../services/stewarding/ReasoningTemplateService.java | 3 +++ .../sustineo/simdesk/services/stewarding/RoundService.java | 4 ++++ .../sustineo/simdesk/services/stewarding/SeriesService.java | 3 +++ .../simdesk/services/stewarding/StewardDecisionService.java | 4 ++++ .../simdesk/services/stewarding/StewardingAppealService.java | 3 +++ .../services/stewarding/StewardingEntrylistService.java | 5 +++++ .../services/stewarding/StewardingIncidentService.java | 3 +++ .../simdesk/services/stewarding/StewardingTrackService.java | 3 +++ 9 files changed, 32 insertions(+) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java index 984c739c..baba086a 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogService.java @@ -5,6 +5,7 @@ import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; import de.sustineo.simdesk.mybatis.mapper.PenaltyDefinitionMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -18,6 +19,7 @@ public class PenaltyCatalogService { private final PenaltyCatalogMapper catalogMapper; private final PenaltyDefinitionMapper definitionMapper; + private final IdGenerator idGenerator; public List getAllCatalogs() { return catalogMapper.findAll(); @@ -29,6 +31,7 @@ public PenaltyCatalog getCatalogById(String id) { @Transactional public void createCatalog(PenaltyCatalog catalog) { + catalog.setId(idGenerator.generateRandomString(12)); catalogMapper.insert(catalog); } @@ -52,6 +55,7 @@ public List getDefinitionsForSessionType(String catalogId, St @Transactional public void createDefinition(PenaltyDefinition definition) { + definition.setId(idGenerator.generateRandomString(12)); definitionMapper.insert(definition); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java index 93834f1a..d2899e4e 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateService.java @@ -3,6 +3,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; import de.sustineo.simdesk.mybatis.mapper.ReasoningTemplateMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -16,6 +17,7 @@ @RequiredArgsConstructor public class ReasoningTemplateService { private final ReasoningTemplateMapper templateMapper; + private final IdGenerator idGenerator; public List getAllTemplates() { return templateMapper.findAll(); @@ -31,6 +33,7 @@ public List getTemplatesByCategory(String category) { @Transactional public void createTemplate(ReasoningTemplate template) { + template.setId(idGenerator.generateRandomString(12)); templateMapper.insert(template); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java index db3198a1..d24936a6 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/RoundService.java @@ -6,6 +6,7 @@ import de.sustineo.simdesk.mybatis.mapper.RoundMapper; import de.sustineo.simdesk.mybatis.mapper.RoundSessionMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -20,6 +21,7 @@ public class RoundService { private final RoundMapper roundMapper; private final RoundSessionMapper sessionMapper; private final StewardingTrackMapper trackMapper; + private final IdGenerator idGenerator; public List getAllRounds() { return roundMapper.findAll(); @@ -39,6 +41,7 @@ public List getRoundsBySeriesId(String seriesId) { @Transactional public void createRound(Round round) { + round.setId(idGenerator.generateRandomString(12)); roundMapper.insert(round); } @@ -62,6 +65,7 @@ public RoundSession getSessionById(String id) { @Transactional public void createSession(RoundSession session) { + session.setId(idGenerator.generateRandomString(12)); sessionMapper.insert(session); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java index cbc15a32..ec3bb74e 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/SeriesService.java @@ -4,6 +4,7 @@ import de.sustineo.simdesk.entities.stewarding.Series; import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; import de.sustineo.simdesk.mybatis.mapper.SeriesMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -17,6 +18,7 @@ public class SeriesService { private final SeriesMapper seriesMapper; private final PenaltyCatalogMapper catalogMapper; + private final IdGenerator idGenerator; public List getAllSeries() { return seriesMapper.findAll(); @@ -32,6 +34,7 @@ public Series getSeriesById(String id) { @Transactional public void createSeries(Series series) { + series.setId(idGenerator.generateRandomString(12)); seriesMapper.insert(series); } diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java index c7976ff1..2c30ee30 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardDecisionService.java @@ -5,6 +5,7 @@ import de.sustineo.simdesk.entities.stewarding.StewardDecision; import de.sustineo.simdesk.mybatis.mapper.StewardDecisionMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -18,6 +19,7 @@ public class StewardDecisionService { private final StewardDecisionMapper decisionMapper; private final StewardingIncidentMapper incidentMapper; + private final IdGenerator idGenerator; public StewardDecision getDecisionById(String id) { return decisionMapper.findById(id); @@ -42,6 +44,7 @@ public List getManualDecisionsBySessionId(String sessionId) { @Transactional public void makeDecision(StewardDecision decision) { + decision.setId(idGenerator.generateRandomString(12)); decisionMapper.insert(decision); if (decision.getIncidentId() != null) { incidentMapper.updateStatus(decision.getIncidentId(), IncidentStatus.DECISION_MADE.name()); @@ -50,6 +53,7 @@ public void makeDecision(StewardDecision decision) { @Transactional public void reviseDecision(String oldDecisionId, StewardDecision newDecision) { + newDecision.setId(idGenerator.generateRandomString(12)); decisionMapper.deactivate(oldDecisionId); decisionMapper.insert(newDecision); decisionMapper.setSupersededBy(oldDecisionId, newDecision.getId()); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java index b886234e..740f64d4 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingAppealService.java @@ -8,6 +8,7 @@ import de.sustineo.simdesk.mybatis.mapper.StewardDecisionMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingAppealMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ public class StewardingAppealService { private final StewardingAppealMapper appealMapper; private final StewardingIncidentMapper incidentMapper; private final StewardDecisionMapper decisionMapper; + private final IdGenerator idGenerator; public List getAppealsByDecisionId(String decisionId) { return appealMapper.findByDecisionId(decisionId); @@ -33,6 +35,7 @@ public Appeal getAppealById(String id) { @Transactional public void fileAppeal(Appeal appeal) { + appeal.setId(idGenerator.generateRandomString(12)); appealMapper.insert(appeal); StewardDecision decision = decisionMapper.findById(appeal.getDecisionId()); if (decision != null && decision.getIncidentId() != null) { diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java index 8dcb9424..a8c8f63c 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistService.java @@ -9,6 +9,7 @@ import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistDriverMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistEntryMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -24,6 +25,7 @@ public class StewardingEntrylistService { private final StewardingEntrylistEntryMapper entryMapper; private final StewardingEntrylistDriverMapper driverMapper; private final ObjectMapper objectMapper; + private final IdGenerator idGenerator; public StewardingEntrylist getEntrylistByRoundId(String roundId) { List entrylists = entrylistMapper.findByRoundId(roundId); @@ -50,6 +52,7 @@ public void uploadEntrylistForRound(String roundId, String jsonContent) { } StewardingEntrylist entrylist = new StewardingEntrylist(); + entrylist.setId(idGenerator.generateRandomString(12)); entrylist.setRoundId(roundId); entrylist.setRawJson(jsonContent); entrylistMapper.insert(entrylist); @@ -76,6 +79,7 @@ private void parseAndInsertEntries(JsonNode root, StewardingEntrylist entrylist) } StewardingEntrylistEntry entry = new StewardingEntrylistEntry(); + entry.setId(idGenerator.generateRandomString(12)); entry.setEntrylistId(entrylist.getId()); entry.setRaceNumber(raceNumber); entry.setCarModelId(forcedCarModel); @@ -85,6 +89,7 @@ private void parseAndInsertEntries(JsonNode root, StewardingEntrylist entrylist) if (driversNode != null && driversNode.isArray()) { for (JsonNode driverNode : driversNode) { StewardingEntrylistDriver driver = new StewardingEntrylistDriver(); + driver.setId(idGenerator.generateRandomString(12)); driver.setEntryId(entry.getId()); driver.setFirstName(driverNode.path("firstName").asText("")); driver.setLastName(driverNode.path("lastName").asText("")); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java index 5d7630d3..777838d9 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingIncidentService.java @@ -5,6 +5,7 @@ import de.sustineo.simdesk.entities.stewarding.IncidentStatus; import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentInvolvedEntryMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingIncidentMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -18,6 +19,7 @@ public class StewardingIncidentService { private final StewardingIncidentMapper incidentMapper; private final StewardingIncidentInvolvedEntryMapper involvedEntryMapper; + private final IdGenerator idGenerator; public List getIncidentsBySessionId(String sessionId) { return incidentMapper.findBySessionId(sessionId); @@ -33,6 +35,7 @@ public List getIncidentsBySessionIdAndStatus(String sessionId, Inciden @Transactional public void createIncident(Incident incident, List involvedEntryIds) { + incident.setId(idGenerator.generateRandomString(12)); incidentMapper.insert(incident); for (String entryId : involvedEntryIds) { involvedEntryMapper.insert(incident.getId(), entryId); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java index 7e063890..709adb8d 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/services/stewarding/StewardingTrackService.java @@ -3,6 +3,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.StewardingTrack; import de.sustineo.simdesk.mybatis.mapper.StewardingTrackMapper; +import de.sustineo.simdesk.services.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -15,6 +16,7 @@ @RequiredArgsConstructor public class StewardingTrackService { private final StewardingTrackMapper trackMapper; + private final IdGenerator idGenerator; public List getAllTracks() { return trackMapper.findAll(); @@ -26,6 +28,7 @@ public StewardingTrack getTrackById(String id) { @Transactional public void createTrack(StewardingTrack track) { + track.setId(idGenerator.generateRandomString(12)); trackMapper.insert(track); } From 224f9cf6d2736c0365f99517b1afcc9e26707892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:55:49 +0000 Subject: [PATCH 22/24] refactor: replace Notification.show with NotificationService and page reload with grid refresh in stewarding views Replace raw Notification.show() calls with NotificationService methods (showSuccessNotification/showErrorNotification) across all 8 stewarding view files. Replace ui.getPage().reload() with grid.setItems() for list views and ui.navigate() for detail views to avoid full page reloads. Files changed: - SeriesListView: grid refresh after series creation - StewardingTrackListView: grid refresh after track creation - PenaltyCatalogListView: grid refresh after catalog creation - ReasoningTemplateListView: grid refresh after template creation - PenaltyCatalogDetailView: grid refresh after penalty definition added - SeriesDetailView: navigate for series update, grid refresh for rounds - RoundDetailView: navigate for session/incident/entrylist/round changes - IncidentDetailView: navigate for decision save Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../views/stewarding/IncidentDetailView.java | 22 ++++---- .../stewarding/PenaltyCatalogDetailView.java | 21 ++++---- .../stewarding/PenaltyCatalogListView.java | 18 +++---- .../stewarding/ReasoningTemplateListView.java | 18 +++---- .../views/stewarding/RoundDetailView.java | 53 +++++++++---------- .../views/stewarding/SeriesDetailView.java | 51 +++++++++--------- .../views/stewarding/SeriesListView.java | 18 +++---- .../stewarding/StewardingTrackListView.java | 18 +++---- 8 files changed, 111 insertions(+), 108 deletions(-) diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java index b40b74f5..e0ba5b9b 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/IncidentDetailView.java @@ -10,8 +10,6 @@ import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; @@ -25,6 +23,7 @@ import de.sustineo.simdesk.entities.auth.UserRoleEnum; import de.sustineo.simdesk.entities.stewarding.*; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.*; import de.sustineo.simdesk.views.BaseView; @@ -45,12 +44,16 @@ public class IncidentDetailView extends BaseView { private final RoundService roundService; private final StewardingEntrylistService entrylistService; private final SecurityService securityService; + private final NotificationService notificationService; + private String seriesId; + private String roundId; + private String incidentId; public IncidentDetailView(StewardingIncidentService incidentService, StewardDecisionService decisionService, StewardingAppealService appealService, PenaltyCatalogService catalogService, ReasoningTemplateService templateService, SeriesService seriesService, RoundService roundService, StewardingEntrylistService entrylistService, - SecurityService securityService) { + SecurityService securityService, NotificationService notificationService) { this.incidentService = incidentService; this.decisionService = decisionService; this.appealService = appealService; @@ -60,6 +63,7 @@ public IncidentDetailView(StewardingIncidentService incidentService, StewardDeci this.roundService = roundService; this.entrylistService = entrylistService; this.securityService = securityService; + this.notificationService = notificationService; } @Override @@ -82,9 +86,9 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - String seriesId = seriesIdParam; - String roundId = roundIdParam; - String incidentId = incidentIdParam; + seriesId = seriesIdParam; + roundId = roundIdParam; + incidentId = incidentIdParam; Series series = seriesService.getSeriesById(seriesId); Round round = roundService.getRoundById(roundId); @@ -235,9 +239,9 @@ private FormLayout createDecisionForm(Incident incident, Series series, String e decisionService.makeDecision(decision); } - Notification.show("Decision saved", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Decision saved"); + getUI().ifPresent(ui -> ui.navigate(IncidentDetailView.class, + new RouteParameters(new RouteParam("seriesId", seriesId), new RouteParam("roundId", roundId), new RouteParam("incidentId", incidentId)))); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); form.add(saveButton); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java index 6390fd6f..68481340 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogDetailView.java @@ -8,8 +8,6 @@ import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Paragraph; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.IntegerField; @@ -21,6 +19,7 @@ import de.sustineo.simdesk.entities.stewarding.PenaltyDefinition; import de.sustineo.simdesk.entities.stewarding.PenaltySessionType; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.views.BaseView; import jakarta.annotation.security.RolesAllowed; @@ -33,9 +32,13 @@ @RolesAllowed({"ADMIN", "STEWARD"}) public class PenaltyCatalogDetailView extends BaseView { private final PenaltyCatalogService catalogService; + private final NotificationService notificationService; + private Grid grid; + private String catalogId; - public PenaltyCatalogDetailView(PenaltyCatalogService catalogService) { + public PenaltyCatalogDetailView(PenaltyCatalogService catalogService, NotificationService notificationService) { this.catalogService = catalogService; + this.notificationService = notificationService; } @Override @@ -56,7 +59,7 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - String catalogId = catalogIdParam; + catalogId = catalogIdParam; PenaltyCatalog catalog = catalogService.getCatalogById(catalogId); if (catalog == null) { @@ -86,7 +89,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(actionLayout); List definitions = catalogService.getDefinitionsByCatalogId(catalogId); - Grid grid = new Grid<>(PenaltyDefinition.class, false); + grid = new Grid<>(PenaltyDefinition.class, false); grid.addColumn(PenaltyDefinition::getCode).setHeader("Code").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.addColumn(PenaltyDefinition::getName).setHeader("Name").setSortable(true); grid.addColumn(PenaltyDefinition::getCategory).setHeader("Category").setAutoWidth(true).setFlexGrow(0).setSortable(true); @@ -135,8 +138,7 @@ private void openPenaltyDialog(String catalogId) { Button saveButton = new Button("Save", e -> { if (codeField.isEmpty() || nameField.isEmpty()) { - Notification.show("Code and Name are required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Code and Name are required"); return; } @@ -153,9 +155,8 @@ private void openPenaltyDialog(String catalogId) { catalogService.createDefinition(definition); dialog.close(); - Notification.show("Penalty definition added", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Penalty definition added"); + grid.setItems(catalogService.getDefinitionsByCatalogId(catalogId)); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java index 94bb4ff8..719513cd 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/PenaltyCatalogListView.java @@ -6,8 +6,6 @@ import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; @@ -16,6 +14,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.views.BaseView; import jakarta.annotation.security.RolesAllowed; @@ -28,9 +27,12 @@ @RolesAllowed({"ADMIN", "STEWARD"}) public class PenaltyCatalogListView extends BaseView { private final PenaltyCatalogService catalogService; + private final NotificationService notificationService; + private Grid grid; - public PenaltyCatalogListView(PenaltyCatalogService catalogService) { + public PenaltyCatalogListView(PenaltyCatalogService catalogService, NotificationService notificationService) { this.catalogService = catalogService; + this.notificationService = notificationService; } @Override @@ -57,7 +59,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(headerLayout); List catalogs = catalogService.getAllCatalogs(); - Grid grid = new Grid<>(PenaltyCatalog.class, false); + grid = new Grid<>(PenaltyCatalog.class, false); grid.addColumn(PenaltyCatalog::getName).setHeader("Name").setSortable(true); grid.addColumn(PenaltyCatalog::getDescription).setHeader("Description").setSortable(true); grid.setItems(catalogs); @@ -92,8 +94,7 @@ private void openNewCatalogDialog() { Button saveButton = new Button("Save", e -> { if (nameField.isEmpty()) { - Notification.show("Name is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Name is required"); return; } @@ -103,9 +104,8 @@ private void openNewCatalogDialog() { .build(); catalogService.createCatalog(catalog); dialog.close(); - Notification.show("Catalog created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Catalog created"); + grid.setItems(catalogService.getAllCatalogs()); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java index d9555398..971a7a73 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/ReasoningTemplateListView.java @@ -6,8 +6,6 @@ import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; @@ -16,6 +14,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.stewarding.ReasoningTemplateService; import de.sustineo.simdesk.views.BaseView; import jakarta.annotation.security.RolesAllowed; @@ -28,9 +27,12 @@ @RolesAllowed({"ADMIN", "STEWARD"}) public class ReasoningTemplateListView extends BaseView { private final ReasoningTemplateService templateService; + private final NotificationService notificationService; + private Grid grid; - public ReasoningTemplateListView(ReasoningTemplateService templateService) { + public ReasoningTemplateListView(ReasoningTemplateService templateService, NotificationService notificationService) { this.templateService = templateService; + this.notificationService = notificationService; } @Override @@ -57,7 +59,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(headerLayout); List templates = templateService.getAllTemplates(); - Grid grid = new Grid<>(ReasoningTemplate.class, false); + grid = new Grid<>(ReasoningTemplate.class, false); grid.addColumn(ReasoningTemplate::getName).setHeader("Name").setSortable(true); grid.addColumn(ReasoningTemplate::getCategory).setHeader("Category").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.addColumn(template -> { @@ -99,8 +101,7 @@ private void openTemplateDialog() { Button saveButton = new Button("Save", e -> { if (nameField.isEmpty()) { - Notification.show("Name is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Name is required"); return; } @@ -112,9 +113,8 @@ private void openTemplateDialog() { templateService.createTemplate(template); dialog.close(); - Notification.show("Template created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Template created"); + grid.setItems(templateService.getAllTemplates()); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java index 58fded44..45ad903d 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/RoundDetailView.java @@ -13,8 +13,6 @@ import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.tabs.TabSheet; @@ -33,6 +31,7 @@ import de.sustineo.simdesk.entities.auth.UserRoleEnum; import de.sustineo.simdesk.entities.stewarding.*; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.RoundService; import de.sustineo.simdesk.services.stewarding.SeriesService; @@ -59,16 +58,21 @@ public class RoundDetailView extends BaseView { private final StewardingEntrylistService entrylistService; private final StewardingTrackService trackService; private final SecurityService securityService; + private final NotificationService notificationService; + private String seriesId; + private String roundId; public RoundDetailView(SeriesService seriesService, RoundService roundService, StewardingIncidentService incidentService, StewardingEntrylistService entrylistService, - StewardingTrackService trackService, SecurityService securityService) { + StewardingTrackService trackService, SecurityService securityService, + NotificationService notificationService) { this.seriesService = seriesService; this.roundService = roundService; this.incidentService = incidentService; this.entrylistService = entrylistService; this.trackService = trackService; this.securityService = securityService; + this.notificationService = notificationService; } @Override @@ -90,8 +94,8 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - String seriesId = seriesIdParam; - String roundId = roundIdParam; + seriesId = seriesIdParam; + roundId = roundIdParam; Series series = seriesService.getSeriesById(seriesId); Round round = roundService.getRoundById(roundId); @@ -230,8 +234,7 @@ private void openAddSessionDialog(String roundId) { Button saveButton = new Button("Save", e -> { if (titleField.isEmpty() || typeCombo.isEmpty()) { - Notification.show("Session type and title are required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Session type and title are required"); return; } @@ -246,9 +249,9 @@ private void openAddSessionDialog(String roundId) { roundService.createSession(session); dialog.close(); - Notification.show("Session created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Session created"); + getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, + new RouteParameters(new RouteParam("seriesId", seriesId), new RouteParam("roundId", roundId)))); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -361,13 +364,11 @@ private void openReportIncidentDialog(String seriesId, String roundId, Series se Button saveButton = new Button("Report", e -> { if (sessionCombo.isEmpty()) { - Notification.show("Session is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Session is required"); return; } if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Title is required"); return; } @@ -394,9 +395,9 @@ private void openReportIncidentDialog(String seriesId, String roundId, Series se incidentService.createIncident(incident, involvedEntryIds); dialog.close(); - Notification.show("Incident reported", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Incident reported"); + getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, + new RouteParameters(new RouteParam("seriesId", seriesId), new RouteParam("roundId", roundId)))); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -420,14 +421,13 @@ private VerticalLayout createEntrylistTab(String roundId) { try { entrylistService.uploadEntrylistForRound(roundId, json); getUI().ifPresent(ui -> ui.access(() -> { - Notification.show("Entrylist uploaded successfully", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - ui.getPage().reload(); + notificationService.showSuccessNotification("Entrylist uploaded successfully"); + ui.navigate(RoundDetailView.class, + new RouteParameters(new RouteParam("seriesId", seriesId), new RouteParam("roundId", roundId))); })); } catch (IllegalArgumentException ex) { getUI().ifPresent(ui -> ui.access(() -> - Notification.show("Invalid entrylist JSON: " + ex.getMessage(), 5000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR) + notificationService.showErrorNotification("Invalid entrylist JSON: " + ex.getMessage()) )); } })); @@ -493,8 +493,7 @@ private void openEditRoundDialog(Round round, String seriesId) { Button saveButton = new Button("Save", e -> { if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Title is required"); return; } @@ -505,9 +504,9 @@ private void openEditRoundDialog(Round round, String seriesId) { roundService.updateRound(round); dialog.close(); - Notification.show("Round updated", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Round updated"); + getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, + new RouteParameters(new RouteParam("seriesId", seriesId), new RouteParam("roundId", roundId)))); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java index 8f18aeda..16961a98 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesDetailView.java @@ -11,8 +11,6 @@ import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; @@ -29,6 +27,7 @@ import de.sustineo.simdesk.entities.stewarding.Series; import de.sustineo.simdesk.entities.stewarding.StewardingTrack; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.services.stewarding.RoundService; @@ -48,15 +47,19 @@ public class SeriesDetailView extends BaseView { private final StewardingTrackService trackService; private final PenaltyCatalogService catalogService; private final SecurityService securityService; + private final NotificationService notificationService; + private Grid roundsGrid; + private String seriesId; public SeriesDetailView(SeriesService seriesService, RoundService roundService, StewardingTrackService trackService, PenaltyCatalogService catalogService, - SecurityService securityService) { + SecurityService securityService, NotificationService notificationService) { this.seriesService = seriesService; this.roundService = roundService; this.trackService = trackService; this.catalogService = catalogService; this.securityService = securityService; + this.notificationService = notificationService; } @Override @@ -77,7 +80,7 @@ public void beforeEnter(BeforeEnterEvent event) { return; } - String seriesId = seriesIdParam; + seriesId = seriesIdParam; Series series = seriesService.getSeriesById(seriesId); if (series == null) { @@ -147,18 +150,18 @@ public void beforeEnter(BeforeEnterEvent event) { add(roundsHeader); List rounds = roundService.getRoundsBySeriesId(seriesId); - Grid grid = new Grid<>(Round.class, false); - grid.addColumn(Round::getTitle).setHeader("Title").setSortable(true); - grid.addColumn(round -> round.getTrack() != null ? round.getTrack().getName() : "-") + roundsGrid = new Grid<>(Round.class, false); + roundsGrid.addColumn(Round::getTitle).setHeader("Title").setSortable(true); + roundsGrid.addColumn(round -> round.getTrack() != null ? round.getTrack().getName() : "-") .setHeader("Track").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(Round::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.addColumn(Round::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); - grid.setItems(rounds); - grid.setSizeFull(); - grid.setSelectionMode(Grid.SelectionMode.NONE); - grid.setColumnReorderingAllowed(true); - grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - grid.addItemClickListener(e -> + roundsGrid.addColumn(Round::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + roundsGrid.addColumn(Round::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); + roundsGrid.setItems(rounds); + roundsGrid.setSizeFull(); + roundsGrid.setSelectionMode(Grid.SelectionMode.NONE); + roundsGrid.setColumnReorderingAllowed(true); + roundsGrid.addThemeVariants(GridVariant.LUMO_NO_BORDER); + roundsGrid.addItemClickListener(e -> getUI().ifPresent(ui -> ui.navigate(RoundDetailView.class, new RouteParameters( new RouteParam("seriesId", String.valueOf(seriesId)), @@ -166,7 +169,7 @@ public void beforeEnter(BeforeEnterEvent event) { ))) ); - addAndExpand(grid); + addAndExpand(roundsGrid); } private HorizontalLayout createDetailRow(String label, String value) { @@ -233,8 +236,7 @@ private void openEditSeriesDialog(Series series) { Button saveButton = new Button("Save", e -> { if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Title is required"); return; } @@ -248,9 +250,8 @@ private void openEditSeriesDialog(Series series) { seriesService.updateSeries(series); dialog.close(); - Notification.show("Series updated", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Series updated"); + getUI().ifPresent(ui -> ui.navigate(SeriesDetailView.class, new RouteParameters("seriesId", seriesId))); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -294,8 +295,7 @@ private void openNewRoundDialog(String seriesId) { Button saveButton = new Button("Save", e -> { if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Title is required"); return; } @@ -309,9 +309,8 @@ private void openNewRoundDialog(String seriesId) { roundService.createRound(round); dialog.close(); - Notification.show("Round created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Round created"); + roundsGrid.setItems(roundService.getRoundsBySeriesId(seriesId)); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java index 8e027055..9c9362ff 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/SeriesListView.java @@ -9,8 +9,6 @@ import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; @@ -23,6 +21,7 @@ import de.sustineo.simdesk.entities.stewarding.PenaltyCatalog; import de.sustineo.simdesk.entities.stewarding.Series; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.auth.SecurityService; import de.sustineo.simdesk.services.stewarding.PenaltyCatalogService; import de.sustineo.simdesk.services.stewarding.SeriesService; @@ -38,12 +37,15 @@ public class SeriesListView extends BaseView { private final SeriesService seriesService; private final PenaltyCatalogService catalogService; private final SecurityService securityService; + private final NotificationService notificationService; + private Grid grid; public SeriesListView(SeriesService seriesService, PenaltyCatalogService catalogService, - SecurityService securityService) { + SecurityService securityService, NotificationService notificationService) { this.seriesService = seriesService; this.catalogService = catalogService; this.securityService = securityService; + this.notificationService = notificationService; } @Override @@ -72,7 +74,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(headerLayout); List seriesList = seriesService.getAllSeries(); - Grid grid = new Grid<>(Series.class, false); + grid = new Grid<>(Series.class, false); grid.addColumn(Series::getTitle).setHeader("Title").setSortable(true); grid.addColumn(Series::getStartDate).setHeader("Start Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.addColumn(Series::getEndDate).setHeader("End Date").setAutoWidth(true).setFlexGrow(0).setSortable(true); @@ -133,8 +135,7 @@ private void openNewSeriesDialog() { Button saveButton = new Button("Save", e -> { if (titleField.isEmpty()) { - Notification.show("Title is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Title is required"); return; } @@ -150,9 +151,8 @@ private void openNewSeriesDialog() { seriesService.createSeries(series); dialog.close(); - Notification.show("Series created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Series created"); + grid.setItems(seriesService.getAllSeries()); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); diff --git a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java index 4970f118..79d4b5cc 100644 --- a/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java +++ b/simdesk-web/src/main/java/de/sustineo/simdesk/views/stewarding/StewardingTrackListView.java @@ -6,8 +6,6 @@ import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridVariant; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.BeforeEnterEvent; @@ -15,6 +13,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.StewardingTrack; import de.sustineo.simdesk.layouts.MainLayout; +import de.sustineo.simdesk.services.NotificationService; import de.sustineo.simdesk.services.stewarding.StewardingTrackService; import de.sustineo.simdesk.views.BaseView; import jakarta.annotation.security.RolesAllowed; @@ -27,9 +26,12 @@ @RolesAllowed({"ADMIN", "STEWARD"}) public class StewardingTrackListView extends BaseView { private final StewardingTrackService trackService; + private final NotificationService notificationService; + private Grid grid; - public StewardingTrackListView(StewardingTrackService trackService) { + public StewardingTrackListView(StewardingTrackService trackService, NotificationService notificationService) { this.trackService = trackService; + this.notificationService = notificationService; } @Override @@ -56,7 +58,7 @@ public void beforeEnter(BeforeEnterEvent event) { add(headerLayout); List tracks = trackService.getAllTracks(); - Grid grid = new Grid<>(StewardingTrack.class, false); + grid = new Grid<>(StewardingTrack.class, false); grid.addColumn(StewardingTrack::getName).setHeader("Name").setSortable(true); grid.addColumn(StewardingTrack::getCountry).setHeader("Country").setAutoWidth(true).setFlexGrow(0).setSortable(true); grid.setItems(tracks); @@ -93,8 +95,7 @@ private void openNewTrackDialog() { Button saveButton = new Button("Save", e -> { if (nameField.isEmpty()) { - Notification.show("Name is required", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + notificationService.showErrorNotification("Name is required"); return; } @@ -106,9 +107,8 @@ private void openNewTrackDialog() { trackService.createTrack(track); dialog.close(); - Notification.show("Track created", 3000, Notification.Position.MIDDLE) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.getPage().reload()); + notificationService.showSuccessNotification("Track created"); + grid.setItems(trackService.getAllTracks()); }); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); From 7ffd245e29bbe673f06508269aa7d4ad34bf1cea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:58:58 +0000 Subject: [PATCH 23/24] Update stewarding test files to use String IDs and mock IdGenerator - StewardingEntrylistServiceTest: use 'round1' instead of '1', add IdGenerator mock - PenaltyCatalogServiceTest: use descriptive String IDs, add IdGenerator mock - ReasoningTemplateServiceTest: use descriptive String IDs, add IdGenerator mock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../stewarding/PenaltyCatalogServiceTest.java | 14 +++++++++----- .../ReasoningTemplateServiceTest.java | 8 ++++++-- .../StewardingEntrylistServiceTest.java | 18 ++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java index 4c3339bb..598984e7 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/PenaltyCatalogServiceTest.java @@ -6,6 +6,7 @@ import de.sustineo.simdesk.entities.stewarding.PenaltySessionType; import de.sustineo.simdesk.mybatis.mapper.PenaltyCatalogMapper; import de.sustineo.simdesk.mybatis.mapper.PenaltyDefinitionMapper; +import de.sustineo.simdesk.services.IdGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -33,21 +34,24 @@ class PenaltyCatalogServiceTest { @MockitoBean private PenaltyDefinitionMapper penaltyDefinitionMapper; + @MockitoBean + private IdGenerator idGenerator; + @Test void getDefinitionsForSessionType_shouldReturnMatchingDefinitions() { PenaltyDefinition racePenalty = PenaltyDefinition.builder() - .id("1") - .catalogId("1") + .id("def123456789") + .catalogId("cat123456789") .code("PEN-001") .name("Causing a collision") .sessionType(PenaltySessionType.RACE) .defaultPenalty("5 second time penalty") .build(); - when(penaltyDefinitionMapper.findByCatalogIdAndSessionType("1", "RACE")) + when(penaltyDefinitionMapper.findByCatalogIdAndSessionType("cat123456789", "RACE")) .thenReturn(List.of(racePenalty)); - List result = penaltyCatalogService.getDefinitionsForSessionType("1", "RACE"); + List result = penaltyCatalogService.getDefinitionsForSessionType("cat123456789", "RACE"); assertThat(result).hasSize(1); assertThat(result.getFirst().getName()).isEqualTo("Causing a collision"); @@ -56,7 +60,7 @@ void getDefinitionsForSessionType_shouldReturnMatchingDefinitions() { @Test void getAllCatalogs_shouldReturnAll() { PenaltyCatalog catalog = PenaltyCatalog.builder() - .id("1") + .id("cat123456789") .name("2025 Season Rules") .description("Standard penalty rules for 2025") .build(); diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java index 16ac8805..02766869 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/ReasoningTemplateServiceTest.java @@ -3,6 +3,7 @@ import de.sustineo.simdesk.configuration.SpringProfile; import de.sustineo.simdesk.entities.stewarding.ReasoningTemplate; import de.sustineo.simdesk.mybatis.mapper.ReasoningTemplateMapper; +import de.sustineo.simdesk.services.IdGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -28,6 +29,9 @@ class ReasoningTemplateServiceTest { @MockitoBean private ReasoningTemplateMapper reasoningTemplateMapper; + @MockitoBean + private IdGenerator idGenerator; + @Test void renderTemplate_shouldReplacePlaceholders() { String template = "Car {car_a} made contact with Car {car_b} at {location}."; @@ -65,7 +69,7 @@ void renderTemplate_shouldHandleEmptyVariables() { @Test void getAllTemplates_shouldReturnAll() { ReasoningTemplate template = ReasoningTemplate.builder() - .id("1") + .id("tmpl12345678") .name("Test Template") .category("Contact") .templateText("Template text") @@ -83,7 +87,7 @@ void getAllTemplates_shouldReturnAll() { @Test void getTemplatesByCategory_shouldFilterByCategory() { ReasoningTemplate template = ReasoningTemplate.builder() - .id("1") + .id("tmpl12345678") .name("Contact Template") .category("Contact") .templateText("Contact template text") diff --git a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java index 6c491a09..7ccf0b2f 100644 --- a/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java +++ b/simdesk-web/src/test/java/de/sustineo/simdesk/services/stewarding/StewardingEntrylistServiceTest.java @@ -8,6 +8,7 @@ import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistDriverMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistEntryMapper; import de.sustineo.simdesk.mybatis.mapper.StewardingEntrylistMapper; +import de.sustineo.simdesk.services.IdGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -39,9 +40,13 @@ class StewardingEntrylistServiceTest { @MockitoBean private StewardingEntrylistDriverMapper entrylistDriverMapper; + @MockitoBean + private IdGenerator idGenerator; + @Test void uploadEntrylistForRound_shouldParseValidAccJson() { - when(entrylistMapper.findByRoundId("1")).thenReturn(Collections.emptyList()); + when(entrylistMapper.findByRoundId("round1")).thenReturn(Collections.emptyList()); + when(idGenerator.generateRandomString(12)).thenReturn("testid123456"); String accJson = """ { @@ -86,9 +91,9 @@ void uploadEntrylistForRound_shouldParseValidAccJson() { } """; - entrylistService.uploadEntrylistForRound("1", accJson); + entrylistService.uploadEntrylistForRound("round1", accJson); - verify(entrylistMapper).deleteByRoundId("1"); + verify(entrylistMapper).deleteByRoundId("round1"); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, times(2)).insert(any(StewardingEntrylistEntry.class)); verify(entrylistDriverMapper, times(3)).insert(any(StewardingEntrylistDriver.class)); @@ -96,7 +101,8 @@ void uploadEntrylistForRound_shouldParseValidAccJson() { @Test void uploadEntrylistForRound_shouldHandleEmptyEntries() { - when(entrylistMapper.findByRoundId("1")).thenReturn(Collections.emptyList()); + when(entrylistMapper.findByRoundId("round1")).thenReturn(Collections.emptyList()); + when(idGenerator.generateRandomString(12)).thenReturn("testid123456"); String accJson = """ { @@ -105,9 +111,9 @@ void uploadEntrylistForRound_shouldHandleEmptyEntries() { } """; - entrylistService.uploadEntrylistForRound("1", accJson); + entrylistService.uploadEntrylistForRound("round1", accJson); - verify(entrylistMapper).deleteByRoundId("1"); + verify(entrylistMapper).deleteByRoundId("round1"); verify(entrylistMapper).insert(any(StewardingEntrylist.class)); verify(entrylistEntryMapper, never()).insert(any()); verify(entrylistDriverMapper, never()).insert(any()); From eb090e04a6294497d25c14def71dde6de54eeb51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:31:49 +0000 Subject: [PATCH 24/24] Combine Flyway migrations V2_11_0..V2_15_0 into single V2_11_0 per database Co-authored-by: fabieu <43068791+fabieu@users.noreply.github.com> --- .../postgres/V2_11_0__stewarding.sql | 175 ++++++++------ .../postgres/V2_12_0__stewarding_roles.sql | 5 - ..._13_0__stewarding_entrylist_to_session.sql | 9 - ...0__stewarding_series_round_restructure.sql | 27 --- .../V2_15_0__stewarding_string_ids.sql | 225 ------------------ .../migration/sqlite/V2_11_0__stewarding.sql | 175 ++++++++------ .../sqlite/V2_12_0__stewarding_roles.sql | 5 - ..._13_0__stewarding_entrylist_to_session.sql | 9 - ...0__stewarding_series_round_restructure.sql | 27 --- .../sqlite/V2_15_0__stewarding_string_ids.sql | 225 ------------------ 10 files changed, 196 insertions(+), 686 deletions(-) delete mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql delete mode 100644 simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql index 19788d18..1360f4f6 100644 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql +++ b/simdesk-web/src/main/resources/db/migration/postgres/V2_11_0__stewarding.sql @@ -1,37 +1,38 @@ --- Stewarding module tables +-- Stewarding module: tracks, penalty catalogs, definitions, reasoning templates, +-- series, rounds, sessions, entrylist, incidents, decisions, appeals, roles. CREATE TABLE IF NOT EXISTS simdesk.stewarding_track ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, country VARCHAR(100), map_image_url VARCHAR(500), map_metadata TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS simdesk.stewarding_penalty_catalog ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, description TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS simdesk.stewarding_penalty_definition ( - id SERIAL PRIMARY KEY, - catalog_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + catalog_id VARCHAR(12) NOT NULL, code VARCHAR(50), - name VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(100), - session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', default_penalty VARCHAR(255), severity INTEGER, - sort_order INTEGER DEFAULT 0, + sort_order INTEGER DEFAULT 0, FOREIGN KEY (catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) ); @@ -39,63 +40,76 @@ CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON simdesk.stewarding_p CREATE TABLE IF NOT EXISTS simdesk.stewarding_reasoning_template ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, category VARCHAR(100), - template_text TEXT NOT NULL, - sort_order INTEGER DEFAULT 0 + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 ); -CREATE TABLE IF NOT EXISTS simdesk.stewarding_race_weekend +CREATE TABLE IF NOT EXISTS simdesk.stewarding_series ( - id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + title VARCHAR(255) NOT NULL, description TEXT, - track_id INTEGER NOT NULL, - penalty_catalog_id INTEGER NOT NULL, discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id VARCHAR(12), start_date DATE, end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (track_id) REFERENCES simdesk.stewarding_track (id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) ); -CREATE INDEX ix_stewarding_race_weekend_track_id ON simdesk.stewarding_race_weekend (track_id); -CREATE INDEX ix_stewarding_race_weekend_penalty_catalog_id ON simdesk.stewarding_race_weekend (penalty_catalog_id); +CREATE TABLE IF NOT EXISTS simdesk.stewarding_round +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + series_id VARCHAR(12), + track_id VARCHAR(12), + title VARCHAR(255) NOT NULL, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (series_id) REFERENCES simdesk.stewarding_series (id), + FOREIGN KEY (track_id) REFERENCES simdesk.stewarding_track (id) +); + +CREATE INDEX ix_stewarding_round_series_id ON simdesk.stewarding_round (series_id); +CREATE INDEX ix_stewarding_round_track_id ON simdesk.stewarding_round (track_id); CREATE TABLE IF NOT EXISTS simdesk.stewarding_session ( - id SERIAL PRIMARY KEY, - race_weekend_id INTEGER NOT NULL, - session_type VARCHAR(20) NOT NULL, - title VARCHAR(255), - start_time TIMESTAMP, - end_time TIMESTAMP, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (race_weekend_id) REFERENCES simdesk.stewarding_race_weekend (id) + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12) NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) ); -CREATE INDEX ix_stewarding_session_race_weekend_id ON simdesk.stewarding_session (race_weekend_id); +CREATE INDEX ix_stewarding_session_round_id ON simdesk.stewarding_session (round_id); CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist ( - id SERIAL PRIMARY KEY, - race_weekend_id INTEGER NOT NULL UNIQUE, - uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - raw_json TEXT, - FOREIGN KEY (race_weekend_id) REFERENCES simdesk.stewarding_race_weekend (id) + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12), + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) ); -CREATE INDEX ix_stewarding_entrylist_race_weekend_id ON simdesk.stewarding_entrylist (race_weekend_id); +CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist_entry ( - id SERIAL PRIMARY KEY, - entrylist_id INTEGER NOT NULL, - race_number INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + entrylist_id VARCHAR(12) NOT NULL, + race_number INTEGER NOT NULL, car_model_id INTEGER, team_name VARCHAR(255), display_name VARCHAR(255), @@ -106,8 +120,8 @@ CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON simdesk.stewarding_en CREATE TABLE IF NOT EXISTS simdesk.stewarding_entrylist_driver ( - id SERIAL PRIMARY KEY, - entry_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + entry_id VARCHAR(12) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), short_name VARCHAR(10), @@ -120,9 +134,9 @@ CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON simdesk.stewarding_entry CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident ( - id SERIAL PRIMARY KEY, - session_id INTEGER NOT NULL, - title VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + session_id VARCHAR(12) NOT NULL, + title VARCHAR(255) NOT NULL, description TEXT, lap INTEGER, timestamp_in_session VARCHAR(100), @@ -130,10 +144,10 @@ CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident map_marker_y DOUBLE PRECISION, video_url VARCHAR(500), involved_cars_text VARCHAR(500), - status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', reported_by_user_id INTEGER, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id) ); @@ -142,8 +156,8 @@ CREATE INDEX ix_stewarding_incident_status ON simdesk.stewarding_incident (statu CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident_involved_entry ( - incident_id INTEGER NOT NULL, - entry_id INTEGER NOT NULL, + incident_id VARCHAR(12) NOT NULL, + entry_id VARCHAR(12) NOT NULL, PRIMARY KEY (incident_id, entry_id), FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id) ON DELETE CASCADE, FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE @@ -151,20 +165,20 @@ CREATE TABLE IF NOT EXISTS simdesk.stewarding_incident_involved_entry CREATE TABLE IF NOT EXISTS simdesk.stewarding_decision ( - id SERIAL PRIMARY KEY, - incident_id INTEGER, - session_id INTEGER NOT NULL, - decided_by_user_id INTEGER, - penalty_definition_id INTEGER, - custom_penalty VARCHAR(255), - reasoning TEXT, - reasoning_template_id INTEGER, - is_no_action BOOLEAN NOT NULL DEFAULT FALSE, - penalized_entry_id INTEGER, - penalized_car_text VARCHAR(255), - decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - superseded_by_id INTEGER, - is_active BOOLEAN NOT NULL DEFAULT TRUE, + id VARCHAR(12) PRIMARY KEY NOT NULL, + incident_id VARCHAR(12), + session_id VARCHAR(12) NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id VARCHAR(12), + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id VARCHAR(12), + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id VARCHAR(12), + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id VARCHAR(12), + is_active BOOLEAN NOT NULL DEFAULT TRUE, FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id), FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id), FOREIGN KEY (penalty_definition_id) REFERENCES simdesk.stewarding_penalty_definition (id), @@ -179,15 +193,15 @@ CREATE INDEX ix_stewarding_decision_is_active ON simdesk.stewarding_decision (is CREATE TABLE IF NOT EXISTS simdesk.stewarding_appeal ( - id SERIAL PRIMARY KEY, - decision_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + decision_id VARCHAR(12) NOT NULL, filed_by_user_id INTEGER, - filed_by_entry_id INTEGER, - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + filed_by_entry_id VARCHAR(12), + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', response TEXT, responded_by_user_id INTEGER, - filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, responded_at TIMESTAMP, FOREIGN KEY (decision_id) REFERENCES simdesk.stewarding_decision (id), FOREIGN KEY (filed_by_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) @@ -195,3 +209,10 @@ CREATE TABLE IF NOT EXISTS simdesk.stewarding_appeal CREATE INDEX ix_stewarding_appeal_decision_id ON simdesk.stewarding_appeal (decision_id); CREATE INDEX ix_stewarding_appeal_status ON simdesk.stewarding_appeal (status); + +-- Stewarding roles +INSERT INTO simdesk.user_role (name, description) +VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); + +INSERT INTO simdesk.user_role (name, description) +VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql deleted file mode 100644 index 80e160f8..00000000 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_12_0__stewarding_roles.sql +++ /dev/null @@ -1,5 +0,0 @@ -INSERT INTO simdesk.user_role (name, description) -VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); - -INSERT INTO simdesk.user_role (name, description) -VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql deleted file mode 100644 index 110c4f53..00000000 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_13_0__stewarding_entrylist_to_session.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Move entrylist from race weekend to session -ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES simdesk.stewarding_session (id); -UPDATE simdesk.stewarding_entrylist SET session_id = (SELECT id FROM simdesk.stewarding_session WHERE race_weekend_id = simdesk.stewarding_entrylist.race_weekend_id ORDER BY sort_order, created_at LIMIT 1); - -DROP INDEX IF EXISTS simdesk.ix_stewarding_entrylist_race_weekend_id; -CREATE INDEX ix_stewarding_entrylist_session_id ON simdesk.stewarding_entrylist (session_id); - --- Add video_url_enabled setting to race weekend -ALTER TABLE simdesk.stewarding_race_weekend ADD COLUMN video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql deleted file mode 100644 index c570a1f1..00000000 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_14_0__stewarding_series_round_restructure.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Restructure stewarding: Race Weekend -> Series + Round + Session (schema only) - --- 1. Create stewarding_series table -CREATE TABLE IF NOT EXISTS simdesk.stewarding_series -( - id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id INTEGER, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) -); - --- 2. Add series_id column to stewarding_race_weekend (acts as round table) -ALTER TABLE simdesk.stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES simdesk.stewarding_series (id); - -CREATE INDEX ix_stewarding_race_weekend_series_id ON simdesk.stewarding_race_weekend (series_id); - --- 3. Add round_id column to stewarding_entrylist -ALTER TABLE simdesk.stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES simdesk.stewarding_race_weekend (id); - -CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); diff --git a/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql b/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql deleted file mode 100644 index 5d673325..00000000 --- a/simdesk-web/src/main/resources/db/migration/postgres/V2_15_0__stewarding_string_ids.sql +++ /dev/null @@ -1,225 +0,0 @@ --- Drop all stewarding tables and recreate with string IDs and stewarding_round table name - -DROP TABLE IF EXISTS simdesk.stewarding_appeal; -DROP TABLE IF EXISTS simdesk.stewarding_decision; -DROP TABLE IF EXISTS simdesk.stewarding_incident_involved_entry; -DROP TABLE IF EXISTS simdesk.stewarding_incident; -DROP TABLE IF EXISTS simdesk.stewarding_entrylist_driver; -DROP TABLE IF EXISTS simdesk.stewarding_entrylist_entry; -DROP TABLE IF EXISTS simdesk.stewarding_entrylist; -DROP TABLE IF EXISTS simdesk.stewarding_session; -DROP TABLE IF EXISTS simdesk.stewarding_race_weekend; -DROP TABLE IF EXISTS simdesk.stewarding_series; -DROP TABLE IF EXISTS simdesk.stewarding_penalty_definition; -DROP TABLE IF EXISTS simdesk.stewarding_penalty_catalog; -DROP TABLE IF EXISTS simdesk.stewarding_reasoning_template; -DROP TABLE IF EXISTS simdesk.stewarding_track; - -CREATE TABLE simdesk.stewarding_track -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - country VARCHAR(100), - map_image_url VARCHAR(500), - map_metadata TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE simdesk.stewarding_penalty_catalog -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE simdesk.stewarding_penalty_definition -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - catalog_id VARCHAR(12) NOT NULL, - code VARCHAR(50), - name VARCHAR(255) NOT NULL, - description TEXT, - category VARCHAR(100), - session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', - default_penalty VARCHAR(255), - severity INTEGER, - sort_order INTEGER DEFAULT 0, - FOREIGN KEY (catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) -); - -CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON simdesk.stewarding_penalty_definition (catalog_id); - -CREATE TABLE simdesk.stewarding_reasoning_template -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - category VARCHAR(100), - template_text TEXT NOT NULL, - sort_order INTEGER DEFAULT 0 -); - -CREATE TABLE simdesk.stewarding_series -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id VARCHAR(12), - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (penalty_catalog_id) REFERENCES simdesk.stewarding_penalty_catalog (id) -); - -CREATE TABLE simdesk.stewarding_round -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - series_id VARCHAR(12), - track_id VARCHAR(12), - title VARCHAR(255) NOT NULL, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (series_id) REFERENCES simdesk.stewarding_series (id), - FOREIGN KEY (track_id) REFERENCES simdesk.stewarding_track (id) -); - -CREATE INDEX ix_stewarding_round_series_id ON simdesk.stewarding_round (series_id); -CREATE INDEX ix_stewarding_round_track_id ON simdesk.stewarding_round (track_id); - -CREATE TABLE simdesk.stewarding_session -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - round_id VARCHAR(12) NOT NULL, - session_type VARCHAR(20) NOT NULL, - title VARCHAR(255), - start_time TIMESTAMP, - end_time TIMESTAMP, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) -); - -CREATE INDEX ix_stewarding_session_round_id ON simdesk.stewarding_session (round_id); - -CREATE TABLE simdesk.stewarding_entrylist -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - round_id VARCHAR(12), - uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - raw_json TEXT, - FOREIGN KEY (round_id) REFERENCES simdesk.stewarding_round (id) -); - -CREATE INDEX ix_stewarding_entrylist_round_id ON simdesk.stewarding_entrylist (round_id); - -CREATE TABLE simdesk.stewarding_entrylist_entry -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - entrylist_id VARCHAR(12) NOT NULL, - race_number INTEGER NOT NULL, - car_model_id INTEGER, - team_name VARCHAR(255), - display_name VARCHAR(255), - FOREIGN KEY (entrylist_id) REFERENCES simdesk.stewarding_entrylist (id) ON DELETE CASCADE -); - -CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON simdesk.stewarding_entrylist_entry (entrylist_id); - -CREATE TABLE simdesk.stewarding_entrylist_driver -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - entry_id VARCHAR(12) NOT NULL, - first_name VARCHAR(100), - last_name VARCHAR(100), - short_name VARCHAR(10), - steam_id VARCHAR(50), - category INTEGER, - FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE -); - -CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON simdesk.stewarding_entrylist_driver (entry_id); - -CREATE TABLE simdesk.stewarding_incident -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - session_id VARCHAR(12) NOT NULL, - title VARCHAR(255) NOT NULL, - description TEXT, - lap INTEGER, - timestamp_in_session VARCHAR(100), - map_marker_x DOUBLE PRECISION, - map_marker_y DOUBLE PRECISION, - video_url VARCHAR(500), - involved_cars_text VARCHAR(500), - status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', - reported_by_user_id INTEGER, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id) -); - -CREATE INDEX ix_stewarding_incident_session_id ON simdesk.stewarding_incident (session_id); -CREATE INDEX ix_stewarding_incident_status ON simdesk.stewarding_incident (status); - -CREATE TABLE simdesk.stewarding_incident_involved_entry -( - incident_id VARCHAR(12) NOT NULL, - entry_id VARCHAR(12) NOT NULL, - PRIMARY KEY (incident_id, entry_id), - FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id) ON DELETE CASCADE, - FOREIGN KEY (entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) ON DELETE CASCADE -); - -CREATE TABLE simdesk.stewarding_decision -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - incident_id VARCHAR(12), - session_id VARCHAR(12) NOT NULL, - decided_by_user_id INTEGER, - penalty_definition_id VARCHAR(12), - custom_penalty VARCHAR(255), - reasoning TEXT, - reasoning_template_id VARCHAR(12), - is_no_action BOOLEAN NOT NULL DEFAULT FALSE, - penalized_entry_id VARCHAR(12), - penalized_car_text VARCHAR(255), - decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - superseded_by_id VARCHAR(12), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - FOREIGN KEY (incident_id) REFERENCES simdesk.stewarding_incident (id), - FOREIGN KEY (session_id) REFERENCES simdesk.stewarding_session (id), - FOREIGN KEY (penalty_definition_id) REFERENCES simdesk.stewarding_penalty_definition (id), - FOREIGN KEY (reasoning_template_id) REFERENCES simdesk.stewarding_reasoning_template (id), - FOREIGN KEY (penalized_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id), - FOREIGN KEY (superseded_by_id) REFERENCES simdesk.stewarding_decision (id) -); - -CREATE INDEX ix_stewarding_decision_incident_id ON simdesk.stewarding_decision (incident_id); -CREATE INDEX ix_stewarding_decision_session_id ON simdesk.stewarding_decision (session_id); -CREATE INDEX ix_stewarding_decision_is_active ON simdesk.stewarding_decision (is_active); - -CREATE TABLE simdesk.stewarding_appeal -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - decision_id VARCHAR(12) NOT NULL, - filed_by_user_id INTEGER, - filed_by_entry_id VARCHAR(12), - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - response TEXT, - responded_by_user_id INTEGER, - filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - responded_at TIMESTAMP, - FOREIGN KEY (decision_id) REFERENCES simdesk.stewarding_decision (id), - FOREIGN KEY (filed_by_entry_id) REFERENCES simdesk.stewarding_entrylist_entry (id) -); - -CREATE INDEX ix_stewarding_appeal_decision_id ON simdesk.stewarding_appeal (decision_id); -CREATE INDEX ix_stewarding_appeal_status ON simdesk.stewarding_appeal (status); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql index 58a52070..2f795077 100644 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql +++ b/simdesk-web/src/main/resources/db/migration/sqlite/V2_11_0__stewarding.sql @@ -1,37 +1,38 @@ --- Stewarding module tables +-- Stewarding module: tracks, penalty catalogs, definitions, reasoning templates, +-- series, rounds, sessions, entrylist, incidents, decisions, appeals, roles. CREATE TABLE IF NOT EXISTS stewarding_track ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, country VARCHAR(100), map_image_url VARCHAR(500), map_metadata TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS stewarding_penalty_catalog ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, description TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS stewarding_penalty_definition ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - catalog_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + catalog_id VARCHAR(12) NOT NULL, code VARCHAR(50), - name VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(100), - session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', + session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', default_penalty VARCHAR(255), severity INTEGER, - sort_order INTEGER DEFAULT 0, + sort_order INTEGER DEFAULT 0, FOREIGN KEY (catalog_id) REFERENCES stewarding_penalty_catalog (id) ); @@ -39,63 +40,76 @@ CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON stewarding_penalty_d CREATE TABLE IF NOT EXISTS stewarding_reasoning_template ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(255) NOT NULL, category VARCHAR(100), - template_text TEXT NOT NULL, - sort_order INTEGER DEFAULT 0 + template_text TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 ); -CREATE TABLE IF NOT EXISTS stewarding_race_weekend +CREATE TABLE IF NOT EXISTS stewarding_series ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + title VARCHAR(255) NOT NULL, description TEXT, - track_id INTEGER NOT NULL, - penalty_catalog_id INTEGER NOT NULL, discord_webhook_url VARCHAR(500), + video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, + penalty_catalog_id VARCHAR(12), start_date DATE, end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (track_id) REFERENCES stewarding_track (id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) ); -CREATE INDEX ix_stewarding_race_weekend_track_id ON stewarding_race_weekend (track_id); -CREATE INDEX ix_stewarding_race_weekend_penalty_catalog_id ON stewarding_race_weekend (penalty_catalog_id); +CREATE TABLE IF NOT EXISTS stewarding_round +( + id VARCHAR(12) PRIMARY KEY NOT NULL, + series_id VARCHAR(12), + track_id VARCHAR(12), + title VARCHAR(255) NOT NULL, + start_date DATE, + end_date DATE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (series_id) REFERENCES stewarding_series (id), + FOREIGN KEY (track_id) REFERENCES stewarding_track (id) +); + +CREATE INDEX ix_stewarding_round_series_id ON stewarding_round (series_id); +CREATE INDEX ix_stewarding_round_track_id ON stewarding_round (track_id); CREATE TABLE IF NOT EXISTS stewarding_session ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - race_weekend_id INTEGER NOT NULL, - session_type VARCHAR(20) NOT NULL, - title VARCHAR(255), - start_time TIMESTAMP, - end_time TIMESTAMP, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (race_weekend_id) REFERENCES stewarding_race_weekend (id) + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12) NOT NULL, + session_type VARCHAR(20) NOT NULL, + title VARCHAR(255), + start_time TIMESTAMP, + end_time TIMESTAMP, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (round_id) REFERENCES stewarding_round (id) ); -CREATE INDEX ix_stewarding_session_race_weekend_id ON stewarding_session (race_weekend_id); +CREATE INDEX ix_stewarding_session_round_id ON stewarding_session (round_id); CREATE TABLE IF NOT EXISTS stewarding_entrylist ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - race_weekend_id INTEGER NOT NULL UNIQUE, - uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - raw_json TEXT, - FOREIGN KEY (race_weekend_id) REFERENCES stewarding_race_weekend (id) + id VARCHAR(12) PRIMARY KEY NOT NULL, + round_id VARCHAR(12), + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + raw_json TEXT, + FOREIGN KEY (round_id) REFERENCES stewarding_round (id) ); -CREATE INDEX ix_stewarding_entrylist_race_weekend_id ON stewarding_entrylist (race_weekend_id); +CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); CREATE TABLE IF NOT EXISTS stewarding_entrylist_entry ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - entrylist_id INTEGER NOT NULL, - race_number INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + entrylist_id VARCHAR(12) NOT NULL, + race_number INTEGER NOT NULL, car_model_id INTEGER, team_name VARCHAR(255), display_name VARCHAR(255), @@ -106,8 +120,8 @@ CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON stewarding_entrylist_ CREATE TABLE IF NOT EXISTS stewarding_entrylist_driver ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - entry_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + entry_id VARCHAR(12) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), short_name VARCHAR(10), @@ -120,9 +134,9 @@ CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON stewarding_entrylist_dri CREATE TABLE IF NOT EXISTS stewarding_incident ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL, - title VARCHAR(255) NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + session_id VARCHAR(12) NOT NULL, + title VARCHAR(255) NOT NULL, description TEXT, lap INTEGER, timestamp_in_session VARCHAR(100), @@ -130,10 +144,10 @@ CREATE TABLE IF NOT EXISTS stewarding_incident map_marker_y REAL, video_url VARCHAR(500), involved_cars_text VARCHAR(500), - status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', + status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', reported_by_user_id INTEGER, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES stewarding_session (id) ); @@ -142,8 +156,8 @@ CREATE INDEX ix_stewarding_incident_status ON stewarding_incident (status); CREATE TABLE IF NOT EXISTS stewarding_incident_involved_entry ( - incident_id INTEGER NOT NULL, - entry_id INTEGER NOT NULL, + incident_id VARCHAR(12) NOT NULL, + entry_id VARCHAR(12) NOT NULL, PRIMARY KEY (incident_id, entry_id), FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id) ON DELETE CASCADE, FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE @@ -151,20 +165,20 @@ CREATE TABLE IF NOT EXISTS stewarding_incident_involved_entry CREATE TABLE IF NOT EXISTS stewarding_decision ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - incident_id INTEGER, - session_id INTEGER NOT NULL, - decided_by_user_id INTEGER, - penalty_definition_id INTEGER, - custom_penalty VARCHAR(255), - reasoning TEXT, - reasoning_template_id INTEGER, - is_no_action BOOLEAN NOT NULL DEFAULT FALSE, - penalized_entry_id INTEGER, - penalized_car_text VARCHAR(255), - decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - superseded_by_id INTEGER, - is_active BOOLEAN NOT NULL DEFAULT TRUE, + id VARCHAR(12) PRIMARY KEY NOT NULL, + incident_id VARCHAR(12), + session_id VARCHAR(12) NOT NULL, + decided_by_user_id INTEGER, + penalty_definition_id VARCHAR(12), + custom_penalty VARCHAR(255), + reasoning TEXT, + reasoning_template_id VARCHAR(12), + is_no_action BOOLEAN NOT NULL DEFAULT FALSE, + penalized_entry_id VARCHAR(12), + penalized_car_text VARCHAR(255), + decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_by_id VARCHAR(12), + is_active BOOLEAN NOT NULL DEFAULT TRUE, FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id), FOREIGN KEY (session_id) REFERENCES stewarding_session (id), FOREIGN KEY (penalty_definition_id) REFERENCES stewarding_penalty_definition (id), @@ -179,15 +193,15 @@ CREATE INDEX ix_stewarding_decision_is_active ON stewarding_decision (is_active) CREATE TABLE IF NOT EXISTS stewarding_appeal ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - decision_id INTEGER NOT NULL, + id VARCHAR(12) PRIMARY KEY NOT NULL, + decision_id VARCHAR(12) NOT NULL, filed_by_user_id INTEGER, - filed_by_entry_id INTEGER, - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + filed_by_entry_id VARCHAR(12), + reason TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', response TEXT, responded_by_user_id INTEGER, - filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, responded_at TIMESTAMP, FOREIGN KEY (decision_id) REFERENCES stewarding_decision (id), FOREIGN KEY (filed_by_entry_id) REFERENCES stewarding_entrylist_entry (id) @@ -195,3 +209,10 @@ CREATE TABLE IF NOT EXISTS stewarding_appeal CREATE INDEX ix_stewarding_appeal_decision_id ON stewarding_appeal (decision_id); CREATE INDEX ix_stewarding_appeal_status ON stewarding_appeal (status); + +-- Stewarding roles +INSERT INTO user_role (name, description) +VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); + +INSERT INTO user_role (name, description) +VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql deleted file mode 100644 index 9fe375c9..00000000 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_12_0__stewarding_roles.sql +++ /dev/null @@ -1,5 +0,0 @@ -INSERT INTO user_role (name, description) -VALUES ('ROLE_STEWARD', 'Role with stewarding access to manage incidents, decisions and penalties'); - -INSERT INTO user_role (name, description) -VALUES ('ROLE_DRIVER', 'Default role for all users with access to view decisions and file appeals'); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql deleted file mode 100644 index 456aa35b..00000000 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_13_0__stewarding_entrylist_to_session.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Move entrylist from race weekend to session -ALTER TABLE stewarding_entrylist ADD COLUMN session_id INTEGER REFERENCES stewarding_session (id); -UPDATE stewarding_entrylist SET session_id = (SELECT id FROM stewarding_session WHERE race_weekend_id = stewarding_entrylist.race_weekend_id ORDER BY sort_order, created_at LIMIT 1); - -DROP INDEX IF EXISTS ix_stewarding_entrylist_race_weekend_id; -CREATE INDEX ix_stewarding_entrylist_session_id ON stewarding_entrylist (session_id); - --- Add video_url_enabled setting to race weekend -ALTER TABLE stewarding_race_weekend ADD COLUMN video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql deleted file mode 100644 index c73cad4d..00000000 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_14_0__stewarding_series_round_restructure.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Restructure stewarding: Race Weekend -> Series + Round + Session (schema only) - --- 1. Create stewarding_series table -CREATE TABLE IF NOT EXISTS stewarding_series -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id INTEGER, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) -); - --- 2. Add series_id column to stewarding_race_weekend (acts as round table) -ALTER TABLE stewarding_race_weekend ADD COLUMN series_id INTEGER REFERENCES stewarding_series (id); - -CREATE INDEX ix_stewarding_race_weekend_series_id ON stewarding_race_weekend (series_id); - --- 3. Add round_id column to stewarding_entrylist -ALTER TABLE stewarding_entrylist ADD COLUMN round_id INTEGER REFERENCES stewarding_race_weekend (id); - -CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); diff --git a/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql b/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql deleted file mode 100644 index f051a1ff..00000000 --- a/simdesk-web/src/main/resources/db/migration/sqlite/V2_15_0__stewarding_string_ids.sql +++ /dev/null @@ -1,225 +0,0 @@ --- Drop all stewarding tables and recreate with string IDs and stewarding_round table name - -DROP TABLE IF EXISTS stewarding_appeal; -DROP TABLE IF EXISTS stewarding_decision; -DROP TABLE IF EXISTS stewarding_incident_involved_entry; -DROP TABLE IF EXISTS stewarding_incident; -DROP TABLE IF EXISTS stewarding_entrylist_driver; -DROP TABLE IF EXISTS stewarding_entrylist_entry; -DROP TABLE IF EXISTS stewarding_entrylist; -DROP TABLE IF EXISTS stewarding_session; -DROP TABLE IF EXISTS stewarding_race_weekend; -DROP TABLE IF EXISTS stewarding_series; -DROP TABLE IF EXISTS stewarding_penalty_definition; -DROP TABLE IF EXISTS stewarding_penalty_catalog; -DROP TABLE IF EXISTS stewarding_reasoning_template; -DROP TABLE IF EXISTS stewarding_track; - -CREATE TABLE stewarding_track -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - country VARCHAR(100), - map_image_url VARCHAR(500), - map_metadata TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE stewarding_penalty_catalog -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE stewarding_penalty_definition -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - catalog_id VARCHAR(12) NOT NULL, - code VARCHAR(50), - name VARCHAR(255) NOT NULL, - description TEXT, - category VARCHAR(100), - session_type VARCHAR(20) NOT NULL DEFAULT 'ALL', - default_penalty VARCHAR(255), - severity INTEGER, - sort_order INTEGER DEFAULT 0, - FOREIGN KEY (catalog_id) REFERENCES stewarding_penalty_catalog (id) -); - -CREATE INDEX ix_stewarding_penalty_definition_catalog_id ON stewarding_penalty_definition (catalog_id); - -CREATE TABLE stewarding_reasoning_template -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(255) NOT NULL, - category VARCHAR(100), - template_text TEXT NOT NULL, - sort_order INTEGER DEFAULT 0 -); - -CREATE TABLE stewarding_series -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - title VARCHAR(255) NOT NULL, - description TEXT, - discord_webhook_url VARCHAR(500), - video_url_enabled BOOLEAN NOT NULL DEFAULT FALSE, - penalty_catalog_id VARCHAR(12), - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (penalty_catalog_id) REFERENCES stewarding_penalty_catalog (id) -); - -CREATE TABLE stewarding_round -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - series_id VARCHAR(12), - track_id VARCHAR(12), - title VARCHAR(255) NOT NULL, - start_date DATE, - end_date DATE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (series_id) REFERENCES stewarding_series (id), - FOREIGN KEY (track_id) REFERENCES stewarding_track (id) -); - -CREATE INDEX ix_stewarding_round_series_id ON stewarding_round (series_id); -CREATE INDEX ix_stewarding_round_track_id ON stewarding_round (track_id); - -CREATE TABLE stewarding_session -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - round_id VARCHAR(12) NOT NULL, - session_type VARCHAR(20) NOT NULL, - title VARCHAR(255), - start_time TIMESTAMP, - end_time TIMESTAMP, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (round_id) REFERENCES stewarding_round (id) -); - -CREATE INDEX ix_stewarding_session_round_id ON stewarding_session (round_id); - -CREATE TABLE stewarding_entrylist -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - round_id VARCHAR(12), - uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - raw_json TEXT, - FOREIGN KEY (round_id) REFERENCES stewarding_round (id) -); - -CREATE INDEX ix_stewarding_entrylist_round_id ON stewarding_entrylist (round_id); - -CREATE TABLE stewarding_entrylist_entry -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - entrylist_id VARCHAR(12) NOT NULL, - race_number INTEGER NOT NULL, - car_model_id INTEGER, - team_name VARCHAR(255), - display_name VARCHAR(255), - FOREIGN KEY (entrylist_id) REFERENCES stewarding_entrylist (id) ON DELETE CASCADE -); - -CREATE INDEX ix_stewarding_entrylist_entry_entrylist_id ON stewarding_entrylist_entry (entrylist_id); - -CREATE TABLE stewarding_entrylist_driver -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - entry_id VARCHAR(12) NOT NULL, - first_name VARCHAR(100), - last_name VARCHAR(100), - short_name VARCHAR(10), - steam_id VARCHAR(50), - category INTEGER, - FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE -); - -CREATE INDEX ix_stewarding_entrylist_driver_entry_id ON stewarding_entrylist_driver (entry_id); - -CREATE TABLE stewarding_incident -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - session_id VARCHAR(12) NOT NULL, - title VARCHAR(255) NOT NULL, - description TEXT, - lap INTEGER, - timestamp_in_session VARCHAR(100), - map_marker_x REAL, - map_marker_y REAL, - video_url VARCHAR(500), - involved_cars_text VARCHAR(500), - status VARCHAR(30) NOT NULL DEFAULT 'REPORTED', - reported_by_user_id INTEGER, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) REFERENCES stewarding_session (id) -); - -CREATE INDEX ix_stewarding_incident_session_id ON stewarding_incident (session_id); -CREATE INDEX ix_stewarding_incident_status ON stewarding_incident (status); - -CREATE TABLE stewarding_incident_involved_entry -( - incident_id VARCHAR(12) NOT NULL, - entry_id VARCHAR(12) NOT NULL, - PRIMARY KEY (incident_id, entry_id), - FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id) ON DELETE CASCADE, - FOREIGN KEY (entry_id) REFERENCES stewarding_entrylist_entry (id) ON DELETE CASCADE -); - -CREATE TABLE stewarding_decision -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - incident_id VARCHAR(12), - session_id VARCHAR(12) NOT NULL, - decided_by_user_id INTEGER, - penalty_definition_id VARCHAR(12), - custom_penalty VARCHAR(255), - reasoning TEXT, - reasoning_template_id VARCHAR(12), - is_no_action BOOLEAN NOT NULL DEFAULT FALSE, - penalized_entry_id VARCHAR(12), - penalized_car_text VARCHAR(255), - decided_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - superseded_by_id VARCHAR(12), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - FOREIGN KEY (incident_id) REFERENCES stewarding_incident (id), - FOREIGN KEY (session_id) REFERENCES stewarding_session (id), - FOREIGN KEY (penalty_definition_id) REFERENCES stewarding_penalty_definition (id), - FOREIGN KEY (reasoning_template_id) REFERENCES stewarding_reasoning_template (id), - FOREIGN KEY (penalized_entry_id) REFERENCES stewarding_entrylist_entry (id), - FOREIGN KEY (superseded_by_id) REFERENCES stewarding_decision (id) -); - -CREATE INDEX ix_stewarding_decision_incident_id ON stewarding_decision (incident_id); -CREATE INDEX ix_stewarding_decision_session_id ON stewarding_decision (session_id); -CREATE INDEX ix_stewarding_decision_is_active ON stewarding_decision (is_active); - -CREATE TABLE stewarding_appeal -( - id VARCHAR(12) PRIMARY KEY NOT NULL, - decision_id VARCHAR(12) NOT NULL, - filed_by_user_id INTEGER, - filed_by_entry_id VARCHAR(12), - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - response TEXT, - responded_by_user_id INTEGER, - filed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - responded_at TIMESTAMP, - FOREIGN KEY (decision_id) REFERENCES stewarding_decision (id), - FOREIGN KEY (filed_by_entry_id) REFERENCES stewarding_entrylist_entry (id) -); - -CREATE INDEX ix_stewarding_appeal_decision_id ON stewarding_appeal (decision_id); -CREATE INDEX ix_stewarding_appeal_status ON stewarding_appeal (status);