Add audit logging for Bewerb changes, implement ZNS B-Satz export, enhance Zeitplan tab with audit log display, export dialog, and clickable Bewerb items, and integrate FixedWidthLineBuilder utility.
This commit is contained in:
@@ -19,6 +19,7 @@ dependencies {
|
||||
implementation(projects.backend.services.billing.billingService)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.znsParser)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
implementation(projects.backend.infrastructure.security)
|
||||
|
||||
@@ -26,7 +27,7 @@ dependencies {
|
||||
implementation(libs.bundles.spring.boot.secure.service)
|
||||
// Common service extras
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on classpath
|
||||
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation(libs.spring.boot.starter.json)
|
||||
implementation(libs.postgresql.driver)
|
||||
@@ -54,6 +55,7 @@ dependencies {
|
||||
// Flyway runtime (provided by BOM, ensure availability in this module)
|
||||
implementation(libs.flyway.core)
|
||||
implementation(libs.flyway.postgresql)
|
||||
implementation(project(":core:zns-parser"))
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
|
||||
+37
-9
@@ -8,13 +8,24 @@ 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.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
|
||||
data class AuditLogEntry(
|
||||
val id: Uuid,
|
||||
val entityType: String,
|
||||
val entityId: Uuid,
|
||||
val action: String,
|
||||
val userId: Uuid?,
|
||||
val timestamp: kotlin.time.Instant,
|
||||
val changesJson: String?
|
||||
)
|
||||
|
||||
class BewerbService(
|
||||
private val repo: BewerbRepository,
|
||||
private val nennungen: NennungRepository,
|
||||
@@ -158,7 +169,7 @@ class BewerbService(
|
||||
|
||||
suspend fun updateZeitplan(id: Uuid, req: UpdateZeitplanRequest): Bewerb {
|
||||
val current = get(id)
|
||||
// Hier erlauben wir Änderungen auch wenn PUBLISHED (da Drag & Drop im Live-Betrieb nötig)
|
||||
// Hier erlauben wir Änderungen, auch wenn PUBLISHED (da Drag & Drop im Live-Betrieb nötig)
|
||||
val updated = current.copy(
|
||||
geplantesDatum = req.geplantesDatum,
|
||||
beginnZeit = req.beginnZeit,
|
||||
@@ -167,18 +178,35 @@ class BewerbService(
|
||||
|
||||
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}\"}}"
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
fun getAuditLog(bewerbId: Uuid): List<AuditLogEntry> = tenantTransaction {
|
||||
AuditLogTable.selectAll()
|
||||
.where { AuditLogTable.entityId eq bewerbId.toJavaUuid() }
|
||||
.orderBy(AuditLogTable.timestamp, SortOrder.DESC)
|
||||
.map {
|
||||
AuditLogEntry(
|
||||
id = Uuid.parse(it[AuditLogTable.id].toString()),
|
||||
entityType = it[AuditLogTable.entityType],
|
||||
entityId = Uuid.parse(it[AuditLogTable.entityId].toString()),
|
||||
action = it[AuditLogTable.action],
|
||||
userId = it[AuditLogTable.userId]?.let { uid -> Uuid.parse(uid.toString()) },
|
||||
timestamp = it[AuditLogTable.timestamp],
|
||||
changesJson = it[AuditLogTable.changesJson]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(id: Uuid) {
|
||||
val current = get(id)
|
||||
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht gelöscht werden")
|
||||
|
||||
+47
@@ -7,6 +7,8 @@ import at.mocode.core.domain.model.BeginnZeitTypE
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnung
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnungCodeE
|
||||
import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import at.mocode.zns.parser.ZnsBewerb
|
||||
import at.mocode.zns.parser.ZnsBewerbParser
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalTime
|
||||
import org.springframework.http.HttpStatus
|
||||
@@ -161,6 +163,16 @@ data class AbteilungsWarnungDto(
|
||||
val oetoParagraph: String?
|
||||
)
|
||||
|
||||
data class AuditLogEntryDto(
|
||||
val id: String,
|
||||
val entityType: String,
|
||||
val entityId: String,
|
||||
val action: String,
|
||||
val userId: String?,
|
||||
val timestamp: String,
|
||||
val changesJson: String?
|
||||
)
|
||||
|
||||
private fun RichterEinsatzDto.toDomain(): RichterEinsatz =
|
||||
RichterEinsatz(
|
||||
funktionaerId = Uuid.parse(this.funktionaerId),
|
||||
@@ -266,4 +278,39 @@ class BewerbeController(
|
||||
val warnungen = service.validateBewerb(uuid)
|
||||
return domainToDto(b, warnungen)
|
||||
}
|
||||
|
||||
@GetMapping("/bewerbe/{id}/audit-log")
|
||||
suspend fun getAuditLog(@PathVariable id: String): List<AuditLogEntryDto> {
|
||||
return service.getAuditLog(Uuid.parse(id)).map {
|
||||
AuditLogEntryDto(
|
||||
id = it.id.toString(),
|
||||
entityType = it.entityType,
|
||||
entityId = it.entityId.toString(),
|
||||
action = it.action,
|
||||
userId = it.userId?.toString(),
|
||||
timestamp = it.timestamp.toString(),
|
||||
changesJson = it.changesJson
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/turniere/{turnierId}/export/zns/b-satz")
|
||||
suspend fun exportZnsBSatz(@PathVariable turnierId: String): String {
|
||||
val turnierUuid = Uuid.parse(turnierId)
|
||||
val bewerbe = service.list(turnierUuid, null, null)
|
||||
val sb = StringBuilder()
|
||||
sb.append("BBEWERBE\r\n")
|
||||
bewerbe.forEach { b ->
|
||||
val znsBewerb = ZnsBewerb(
|
||||
bewerbNummer = b.znsNummer ?: 0,
|
||||
abteilung = b.znsAbteilung ?: 0,
|
||||
name = b.bezeichnung,
|
||||
klasse = b.klasse,
|
||||
kategorie = "", // Wird aktuell nicht in Bewerb gespeichert
|
||||
datum = b.geplantesDatum
|
||||
)
|
||||
sb.append(ZnsBewerbParser.build(znsBewerb)).append("\r\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user