feat(core+frontend): add P2P sync infrastructure with WebSocket support
- **Core Updates:** - Implemented `P2pSyncService` interface with platform-specific WebSocket implementations (`JvmP2pSyncService` and no-op for JS). - Developed `SyncEvent` sealed class hierarchy to handle peer synchronization events (e.g., `PingEvent`, `PongEvent`, `DataChangedEvent`, etc.). - **Frontend Integration:** - Introduced `SyncManager` to manage peer discovery and synchronization, coupled with `NetworkDiscoveryService`. - Updated dependency injection to include `syncModule` for platform-specific sync service initialization. - Enhanced `BewerbViewModel` to support new sync capabilities, including observing sync events and UI updates for connected peers. - **Backend Enhancements:** - Added ZNS-specific fields (`zns_nummer`, `zns_abteilung`) to Bewerb table for idempotent imports. - Introduced import ZNS logic to handle duplicates and align with SyncManager updates. - **UI Improvements:** - Enhanced `TurnierBewerbeTab` with updated dialogs (ZNS imports, sync status) and dynamic previews. - Improved network syncing feedback and error handling in frontend components. - **DB Changes:** - Added migration for new column fields in the Bewerb table with relevant indexing for ZNS import optimizations.
This commit is contained in:
+3
@@ -36,6 +36,9 @@ data class Bewerb(
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int? = null,
|
||||
val znsAbteilung: Int? = null,
|
||||
)
|
||||
|
||||
interface BewerbRepository {
|
||||
|
||||
+10
-1
@@ -72,7 +72,10 @@ class BewerbRepositoryImpl : BewerbRepository {
|
||||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
||||
// Finanzen
|
||||
startgeldCent = row[BewerbTable.startgeldCent],
|
||||
geldpreisAusbezahlt = row[BewerbTable.geldpreisAusbezahlt]
|
||||
geldpreisAusbezahlt = row[BewerbTable.geldpreisAusbezahlt],
|
||||
// ZNS-Integration
|
||||
znsNummer = row[BewerbTable.znsNummer],
|
||||
znsAbteilung = row[BewerbTable.znsAbteilung]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,6 +107,9 @@ class BewerbRepositoryImpl : BewerbRepository {
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
||||
// ZNS-Integration
|
||||
s[BewerbTable.znsNummer] = b.znsNummer
|
||||
s[BewerbTable.znsAbteilung] = b.znsAbteilung
|
||||
s[BewerbTable.createdAt] = now
|
||||
s[BewerbTable.updatedAt] = now
|
||||
}
|
||||
@@ -148,6 +154,9 @@ class BewerbRepositoryImpl : BewerbRepository {
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
||||
// ZNS-Integration
|
||||
s[BewerbTable.znsNummer] = b.znsNummer
|
||||
s[BewerbTable.znsAbteilung] = b.znsAbteilung
|
||||
s[BewerbTable.updatedAt] = now
|
||||
}
|
||||
persistRichterEinsaetze(b.id, b.richterEinsaetze)
|
||||
|
||||
+44
@@ -57,6 +57,50 @@ class BewerbService(
|
||||
|
||||
suspend fun list(turnierId: Uuid, klasse: String?, q: String?): List<Bewerb> = repo.findByTurnierId(turnierId, klasse, q)
|
||||
|
||||
suspend fun importZns(turnierId: Uuid, reqs: List<CreateBewerbRequest>): List<Bewerb> {
|
||||
if (isTurnierPublished(turnierId)) throw LockedException("Turnier ist PUBLISHED – Import nicht möglich")
|
||||
|
||||
val existing = repo.findByTurnierId(turnierId)
|
||||
val results = mutableListOf<Bewerb>()
|
||||
|
||||
reqs.forEach { req ->
|
||||
// Idempotenz-Check: Wenn ZNS-Nummer und Abteilung bereits existieren, überspringen oder updaten?
|
||||
// Hier: Überspringen, wenn bereits vorhanden (einfachste Logik für MVP)
|
||||
val duplicate = existing.find { it.znsNummer == req.znsNummer && it.znsAbteilung == req.znsAbteilung }
|
||||
if (duplicate == null) {
|
||||
val b = Bewerb(
|
||||
id = Uuid.random(),
|
||||
turnierId = turnierId,
|
||||
klasse = req.klasse,
|
||||
hoeheCm = req.hoeheCm,
|
||||
bezeichnung = req.bezeichnung,
|
||||
teilungsTyp = req.teilungsTyp,
|
||||
beschreibung = req.beschreibung,
|
||||
aufgabe = req.aufgabe,
|
||||
aufgabenNummer = req.aufgabenNummer,
|
||||
paraGrade = req.paraGrade,
|
||||
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) },
|
||||
richterEinsaetze = req.richterEinsaetze.map { RichterEinsatz(Uuid.parse(it.funktionaerId), it.position) },
|
||||
geplantesDatum = req.geplantesDatum,
|
||||
beginnZeitTyp = req.beginnZeitTyp,
|
||||
beginnZeit = req.beginnZeit,
|
||||
reitdauerMinuten = req.reitdauerMinuten,
|
||||
umbauMinuten = req.umbauMinuten,
|
||||
besichtigungMinuten = req.besichtigungMinuten,
|
||||
stechenGeplant = req.stechenGeplant,
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||
znsNummer = req.znsNummer,
|
||||
znsAbteilung = req.znsAbteilung
|
||||
)
|
||||
results.add(repo.create(b))
|
||||
} else {
|
||||
results.add(duplicate)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
suspend fun get(id: Uuid): Bewerb = repo.findById(id) ?: throw NoSuchElementException("Bewerb $id nicht gefunden")
|
||||
|
||||
suspend fun update(id: Uuid, req: UpdateBewerbRequest): Bewerb {
|
||||
|
||||
+23
@@ -48,6 +48,10 @@ data class CreateBewerbRequest(
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int? = null,
|
||||
val znsAbteilung: Int? = null,
|
||||
)
|
||||
|
||||
data class UpdateBewerbRequest(
|
||||
@@ -81,6 +85,10 @@ data class UpdateBewerbRequest(
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int? = null,
|
||||
val znsAbteilung: Int? = null,
|
||||
)
|
||||
|
||||
data class BewerbResponse(
|
||||
@@ -115,6 +123,10 @@ data class BewerbResponse(
|
||||
// Finanzen
|
||||
val startgeldCent: Long?,
|
||||
val geldpreisAusbezahlt: Boolean,
|
||||
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int?,
|
||||
val znsAbteilung: Int?,
|
||||
)
|
||||
|
||||
private fun RichterEinsatzDto.toDomain(): RichterEinsatz =
|
||||
@@ -145,6 +157,8 @@ private fun domainToDto(b: Bewerb): BewerbResponse = BewerbResponse(
|
||||
stechenGeplant = b.stechenGeplant,
|
||||
startgeldCent = b.startgeldCent,
|
||||
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
||||
znsNummer = b.znsNummer,
|
||||
znsAbteilung = b.znsAbteilung,
|
||||
)
|
||||
|
||||
@RestController
|
||||
@@ -164,6 +178,15 @@ class BewerbeController(
|
||||
)
|
||||
)
|
||||
|
||||
@PostMapping("/turniere/{turnierId}/bewerbe/import/zns")
|
||||
suspend fun importZns(
|
||||
@PathVariable turnierId: String,
|
||||
@RequestBody body: List<CreateBewerbRequest>
|
||||
): List<BewerbResponse> = service.importZns(
|
||||
Uuid.parse(turnierId),
|
||||
body
|
||||
).map(::domainToDto)
|
||||
|
||||
@GetMapping("/turniere/{turnierId}/bewerbe")
|
||||
suspend fun list(
|
||||
@PathVariable turnierId: String,
|
||||
|
||||
+4
@@ -38,6 +38,10 @@ object BewerbTable : Table("bewerbe") {
|
||||
val startgeldCent = long("startgeld_cent").nullable()
|
||||
val geldpreisAusbezahlt = bool("geldpreis_ausbezahlt").default(false)
|
||||
|
||||
// ZNS-Integration
|
||||
val znsNummer = integer("zns_nummer").nullable()
|
||||
val znsAbteilung = integer("zns_abteilung").nullable()
|
||||
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- V7: ZNS-spezifische Felder für Bewerbe zur Vermeidung von Duplikaten beim Import
|
||||
-- Context: Phase 8 – ZNS Importer Erweiterung
|
||||
|
||||
ALTER TABLE bewerbe
|
||||
ADD COLUMN IF NOT EXISTS zns_nummer INTEGER NULL,
|
||||
ADD COLUMN IF NOT EXISTS zns_abteilung INTEGER NULL;
|
||||
|
||||
-- Index für schnelles Nachschlagen beim Import
|
||||
CREATE INDEX IF NOT EXISTS idx_bewerbe_zns_ref ON bewerbe(turnier_id, zns_nummer, zns_abteilung);
|
||||
Reference in New Issue
Block a user