Add audit logging for Zeitplan updates, implement conflict validation for overlapping schedules and judge assignments, and enhance frontend with detailed warning visualizations in Zeitplan tab.
This commit is contained in:
+5
-1
@@ -44,7 +44,11 @@ enum class AbteilungsWarnungCodeE {
|
||||
/** Vorgeschriebene Abteilungs-Struktur nicht vorhanden */
|
||||
WARN_STRUKTURELLE_TEILUNG_FEHLT,
|
||||
/** Abteilungs-Struktur vorhanden, aber Teilnehmerkreis falsch/unvollständig */
|
||||
WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG
|
||||
WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG,
|
||||
/** Mehrere Bewerbe zur gleichen Zeit am gleichen Platz */
|
||||
WARN_ZEITPLAN_PLATZ_KONFLIKT,
|
||||
/** Richter hat zeitgleiche Einsätze in verschiedenen Bewerben */
|
||||
WARN_ZEITPLAN_RICHTER_KONFLIKT
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+75
@@ -3,7 +3,11 @@
|
||||
package at.mocode.entries.domain.service
|
||||
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnung
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnungCodeE
|
||||
import at.mocode.entries.domain.model.Bewerb
|
||||
import at.mocode.entries.domain.repository.CompetitionRepository
|
||||
import kotlinx.datetime.toJavaLocalTime
|
||||
import java.time.LocalTime
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
@@ -46,9 +50,80 @@ class CompetitionWarningService(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Zeitplan-Konflikte (Turnier-weit)
|
||||
result.putAll(validateZeitplanKonflikte(bewerbe))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun validateZeitplanKonflikte(bewerbe: List<Bewerb>): Map<Uuid, List<AbteilungsWarnung>> {
|
||||
val conflicts = mutableMapOf<Uuid, List<AbteilungsWarnung>>()
|
||||
|
||||
// Nur Bewerbe mit Zeitangabe prüfen
|
||||
val zeitplanBewerbe = bewerbe.filter { it.geplantesDatum != null && it.beginnZeit != null }
|
||||
|
||||
for (b1 in zeitplanBewerbe) {
|
||||
val warnings = mutableListOf<AbteilungsWarnung>()
|
||||
val start1 = b1.beginnZeit!!
|
||||
val ende1 = berechneEnde(b1)
|
||||
|
||||
for (b2 in zeitplanBewerbe) {
|
||||
if (b1.bewerbId == b2.bewerbId) continue
|
||||
if (b1.geplantesDatum != b2.geplantesDatum) continue
|
||||
|
||||
val start2 = b2.beginnZeit!!
|
||||
val ende2 = berechneEnde(b2)
|
||||
|
||||
// Überlappungs-Check
|
||||
if (isOverlapping(start1, ende1, start2, ende2)) {
|
||||
// Platz-Konflikt
|
||||
if (b1.austragungsplatzId != null && b1.austragungsplatzId == b2.austragungsplatzId) {
|
||||
warnings.add(AbteilungsWarnung(
|
||||
code = AbteilungsWarnungCodeE.WARN_ZEITPLAN_PLATZ_KONFLIKT,
|
||||
bewerbId = b1.bewerbId,
|
||||
nachricht = "Zeitliche Überschneidung auf dem gleichen Platz mit Bewerb ${b2.bewerbNummer}",
|
||||
oetoParagraph = "Allgemeine Zeitplanung"
|
||||
))
|
||||
}
|
||||
|
||||
// Richter-Konflikt
|
||||
val gemeinsameRichter = b1.richterEinsaetze.map { it.funktionaerId }
|
||||
.intersect(b2.richterEinsaetze.map { it.funktionaerId }.toSet())
|
||||
|
||||
if (gemeinsameRichter.isNotEmpty()) {
|
||||
warnings.add(AbteilungsWarnung(
|
||||
code = AbteilungsWarnungCodeE.WARN_ZEITPLAN_RICHTER_KONFLIKT,
|
||||
bewerbId = b1.bewerbId,
|
||||
nachricht = "Richter-Doppelbelegung mit Bewerb ${b2.bewerbNummer}",
|
||||
oetoParagraph = "Richter-Einteilung"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (warnings.isNotEmpty()) {
|
||||
conflicts[b1.bewerbId] = (conflicts[b1.bewerbId] ?: emptyList()) + warnings
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
private fun berechneEnde(b: Bewerb): kotlinx.datetime.LocalTime {
|
||||
val start = b.beginnZeit!!
|
||||
val dauer = b.reitdauerMinuten ?: 0
|
||||
val umbau = b.umbauMinuten ?: 0
|
||||
val besichtigung = b.besichtigungMinuten ?: 0
|
||||
val gesamtMinuten = start.hour * 60 + start.minute + dauer + umbau + besichtigung
|
||||
|
||||
val endHour = (gesamtMinuten / 60) % 24
|
||||
val endMin = gesamtMinuten % 60
|
||||
return kotlinx.datetime.LocalTime(endHour, endMin)
|
||||
}
|
||||
|
||||
private fun isOverlapping(s1: kotlinx.datetime.LocalTime, e1: kotlinx.datetime.LocalTime,
|
||||
s2: kotlinx.datetime.LocalTime, e2: kotlinx.datetime.LocalTime): Boolean {
|
||||
return s1 < e2 && s2 < e1
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert einen einzelnen Bewerb und gibt Warnungen zurück.
|
||||
*/
|
||||
|
||||
+14
@@ -6,7 +6,9 @@ import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||
import at.mocode.entries.service.errors.LockedException
|
||||
import at.mocode.entries.service.persistence.AuditLogTable
|
||||
import at.mocode.entries.service.persistence.TurnierTable
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import at.mocode.entries.service.tenant.tenantTransaction
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
@@ -162,6 +164,18 @@ class BewerbService(
|
||||
beginnZeit = req.beginnZeit,
|
||||
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) }
|
||||
)
|
||||
|
||||
tenantTransaction {
|
||||
// Audit-Log schreiben
|
||||
AuditLogTable.insert {
|
||||
it[entityType] = "BEWERB"
|
||||
it[entityId] = id.toJavaUuid()
|
||||
it[action] = "UPDATE_ZEITPLAN"
|
||||
it[timestamp] = kotlin.time.Clock.System.now()
|
||||
it[changesJson] = "{\"old\": {\"datum\": \"${current.geplantesDatum}\", \"zeit\": \"${current.beginnZeit}\", \"platz\": \"${current.austragungsplatzId}\"}, \"new\": {\"datum\": \"${updated.geplantesDatum}\", \"zeit\": \"${updated.beginnZeit}\", \"platz\": \"${updated.austragungsplatzId}\"}}"
|
||||
}
|
||||
}
|
||||
|
||||
return repo.update(updated)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -261,7 +261,9 @@ class BewerbeController(
|
||||
|
||||
@PatchMapping("/bewerbe/{id}/zeitplan")
|
||||
suspend fun updateZeitplan(@PathVariable id: String, @RequestBody body: UpdateZeitplanRequest): BewerbResponse {
|
||||
val b = service.updateZeitplan(Uuid.parse(id), body)
|
||||
return domainToDto(b)
|
||||
val uuid = Uuid.parse(id)
|
||||
val b = service.updateZeitplan(uuid, body)
|
||||
val warnungen = service.validateBewerb(uuid)
|
||||
return domainToDto(b, warnungen)
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
import kotlin.time.Instant
|
||||
|
||||
object AuditLogTable : Table("audit_log") {
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
val entityType = varchar("entity_type", 50)
|
||||
val entityId = javaUUID("entity_id")
|
||||
val action = varchar("action", 50)
|
||||
val userId = javaUUID("user_id").nullable()
|
||||
val timestamp = timestamp("timestamp")
|
||||
val changesJson = text("changes_json").nullable()
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, entityId)
|
||||
index(false, timestamp)
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -39,8 +39,10 @@ class BewerbeZeitplanIntegrationTest {
|
||||
TurnierTable,
|
||||
BewerbTable,
|
||||
AbteilungTable,
|
||||
BewerbRichterEinsatzTable
|
||||
BewerbRichterEinsatzTable,
|
||||
AuditLogTable
|
||||
)
|
||||
AuditLogTable.deleteAll()
|
||||
BewerbRichterEinsatzTable.deleteAll()
|
||||
BewerbTable.deleteAll()
|
||||
AbteilungTable.deleteAll()
|
||||
|
||||
Reference in New Issue
Block a user