From c38270eb58fba2e35251538f116f8ef7b2371a0e Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sun, 29 Jun 2025 23:59:38 +0200 Subject: [PATCH] (fix) RESTful API --- .../at/mocode/model/ArtikelRepository.kt | 14 ++ .../at/mocode/model/PersonRepository.kt | 15 ++ .../mocode/model/PostgresArtikelRepository.kt | 84 +++++++++ .../mocode/model/PostgresPersonRepository.kt | 167 ++++++++++++++++++ .../mocode/model/PostgresVereinRepository.kt | 106 +++++++++++ .../at/mocode/model/VereinRepository.kt | 15 ++ .../kotlin/at/mocode/routes/ArtikelRoutes.kt | 130 ++++++++++++++ .../kotlin/at/mocode/routes/PersonRoutes.kt | 151 ++++++++++++++++ .../kotlin/at/mocode/routes/VereinRoutes.kt | 148 ++++++++++++++++ 9 files changed, 830 insertions(+) create mode 100644 server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/model/PersonRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/model/VereinRepository.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt create mode 100644 server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt diff --git a/server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt b/server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt new file mode 100644 index 00000000..9a16ef26 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/ArtikelRepository.kt @@ -0,0 +1,14 @@ +package at.mocode.model + +import at.mocode.shared.model.Artikel +import com.benasher44.uuid.Uuid + +interface ArtikelRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Artikel? + suspend fun create(artikel: Artikel): Artikel + suspend fun update(id: Uuid, artikel: Artikel): Artikel? + suspend fun delete(id: Uuid): Boolean + suspend fun findByVerbandsabgabe(istVerbandsabgabe: Boolean): List + suspend fun search(query: String): List +} diff --git a/server/src/main/kotlin/at/mocode/model/PersonRepository.kt b/server/src/main/kotlin/at/mocode/model/PersonRepository.kt new file mode 100644 index 00000000..2696dbf9 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/PersonRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.model + +import at.mocode.shared.stammdaten.Person +import com.benasher44.uuid.Uuid + +interface PersonRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Person? + suspend fun findByOepsSatzNr(oepsSatzNr: String): Person? + suspend fun create(person: Person): Person + suspend fun update(id: Uuid, person: Person): Person? + suspend fun delete(id: Uuid): Boolean + suspend fun findByVereinId(vereinId: Uuid): List + suspend fun search(query: String): List +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt b/server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt new file mode 100644 index 00000000..5051bbb4 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/PostgresArtikelRepository.kt @@ -0,0 +1,84 @@ +package at.mocode.model + +import at.mocode.shared.model.Artikel +import at.mocode.tables.ArtikelTable +import com.benasher44.uuid.Uuid +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction + +class PostgresArtikelRepository : ArtikelRepository { + + override suspend fun findAll(): List = transaction { + ArtikelTable.selectAll().map { rowToArtikel(it) } + } + + override suspend fun findById(id: Uuid): Artikel? = transaction { + ArtikelTable.select { ArtikelTable.id eq id } + .map { rowToArtikel(it) } + .singleOrNull() + } + + override suspend fun create(artikel: Artikel): Artikel = transaction { + val now = Clock.System.now() + ArtikelTable.insert { + it[id] = artikel.id + it[bezeichnung] = artikel.bezeichnung + it[preis] = artikel.preis.toStringExpanded() + it[einheit] = artikel.einheit + it[istVerbandsabgabe] = artikel.istVerbandsabgabe + it[createdAt] = now + it[updatedAt] = now + } + artikel.copy(createdAt = now, updatedAt = now) + } + + override suspend fun update(id: Uuid, artikel: Artikel): Artikel? = transaction { + val updateCount = ArtikelTable.update({ ArtikelTable.id eq id }) { + it[bezeichnung] = artikel.bezeichnung + it[preis] = artikel.preis.toStringExpanded() + it[einheit] = artikel.einheit + it[istVerbandsabgabe] = artikel.istVerbandsabgabe + it[updatedAt] = Clock.System.now() + } + if (updateCount > 0) { + ArtikelTable.select { ArtikelTable.id eq id } + .map { rowToArtikel(it) } + .singleOrNull() + } else null + } + + override suspend fun delete(id: Uuid): Boolean = transaction { + ArtikelTable.deleteWhere { ArtikelTable.id eq id } > 0 + } + + override suspend fun findByVerbandsabgabe(istVerbandsabgabe: Boolean): List = transaction { + ArtikelTable.select { ArtikelTable.istVerbandsabgabe eq istVerbandsabgabe } + .map { rowToArtikel(it) } + } + + override suspend fun search(query: String): List = transaction { + ArtikelTable.select { + (ArtikelTable.bezeichnung.lowerCase() like "%${query.lowercase()}%") or + (ArtikelTable.einheit.lowerCase() like "%${query.lowercase()}%") + }.map { rowToArtikel(it) } + } + + private fun rowToArtikel(row: ResultRow): Artikel { + return Artikel( + id = row[ArtikelTable.id], + bezeichnung = row[ArtikelTable.bezeichnung], + preis = try { + BigDecimal.parseString(row[ArtikelTable.preis]) + } catch (e: Exception) { + BigDecimal.ZERO + }, + einheit = row[ArtikelTable.einheit], + istVerbandsabgabe = row[ArtikelTable.istVerbandsabgabe], + createdAt = row[ArtikelTable.createdAt], + updatedAt = row[ArtikelTable.updatedAt] + ) + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt b/server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt new file mode 100644 index 00000000..05e22a7c --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/PostgresPersonRepository.kt @@ -0,0 +1,167 @@ +package at.mocode.model + +import at.mocode.shared.enums.FunktionaerRolle +import at.mocode.shared.stammdaten.LizenzInfo +import at.mocode.shared.stammdaten.Person +import at.mocode.tables.PersonenTable +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuidFrom +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction + +class PostgresPersonRepository : PersonRepository { + + override suspend fun findAll(): List = transaction { + PersonenTable.selectAll().map { rowToPerson(it) } + } + + override suspend fun findById(id: Uuid): Person? = transaction { + PersonenTable.select { PersonenTable.id eq id } + .map { rowToPerson(it) } + .singleOrNull() + } + + override suspend fun findByOepsSatzNr(oepsSatzNr: String): Person? = transaction { + PersonenTable.select { PersonenTable.oepsSatzNr eq oepsSatzNr } + .map { rowToPerson(it) } + .singleOrNull() + } + + override suspend fun create(person: Person): Person = transaction { + val now = Clock.System.now() + PersonenTable.insert { + it[id] = person.id + it[oepsSatzNr] = person.oepsSatzNr + it[nachname] = person.nachname + it[vorname] = person.vorname + it[titel] = person.titel + it[geburtsdatum] = person.geburtsdatum + it[geschlecht] = person.geschlechtE + it[nationalitaet] = person.nationalitaet + it[email] = person.email + it[telefon] = person.telefon + it[adresse] = person.adresse + it[plz] = person.plz + it[ort] = person.ort + it[stammVereinId] = person.stammVereinId + it[mitgliedsNummerIntern] = person.mitgliedsNummerIntern + it[letzteZahlungJahr] = person.letzteZahlungJahr + it[feiId] = person.feiId + it[istGesperrt] = person.istGesperrt + it[sperrGrund] = person.sperrGrund + it[rollenCsv] = person.rollen.joinToString(",") { rolle -> rolle.name } + it[qualifikationenRichterCsv] = person.qualifikationenRichter.joinToString(",") + it[qualifikationenParcoursbauerCsv] = person.qualifikationenParcoursbauer.joinToString(",") + it[istAktiv] = person.istAktiv + it[createdAt] = now + it[updatedAt] = now + } + person.copy(createdAt = now, updatedAt = now) + } + + override suspend fun update(id: Uuid, person: Person): Person? = transaction { + val updateCount = PersonenTable.update({ PersonenTable.id eq id }) { + it[nachname] = person.nachname + it[vorname] = person.vorname + it[titel] = person.titel + it[geburtsdatum] = person.geburtsdatum + it[geschlecht] = person.geschlechtE + it[nationalitaet] = person.nationalitaet + it[email] = person.email + it[telefon] = person.telefon + it[adresse] = person.adresse + it[plz] = person.plz + it[ort] = person.ort + it[stammVereinId] = person.stammVereinId + it[mitgliedsNummerIntern] = person.mitgliedsNummerIntern + it[letzteZahlungJahr] = person.letzteZahlungJahr + it[feiId] = person.feiId + it[istGesperrt] = person.istGesperrt + it[sperrGrund] = person.sperrGrund + it[rollenCsv] = person.rollen.joinToString(",") { rolle -> rolle.name } + it[qualifikationenRichterCsv] = person.qualifikationenRichter.joinToString(",") + it[qualifikationenParcoursbauerCsv] = person.qualifikationenParcoursbauer.joinToString(",") + it[istAktiv] = person.istAktiv + it[updatedAt] = Clock.System.now() + } + if (updateCount > 0) { + PersonenTable.select { PersonenTable.id eq id } + .map { rowToPerson(it) } + .singleOrNull() + } else null + } + + override suspend fun delete(id: Uuid): Boolean = transaction { + PersonenTable.deleteWhere { PersonenTable.id eq id } > 0 + } + + override suspend fun findByVereinId(vereinId: Uuid): List = transaction { + PersonenTable.select { PersonenTable.stammVereinId eq vereinId } + .map { rowToPerson(it) } + } + + override suspend fun search(query: String): List = transaction { + PersonenTable.select { + (PersonenTable.nachname.lowerCase() like "%${query.lowercase()}%") or + (PersonenTable.vorname.lowerCase() like "%${query.lowercase()}%") or + (PersonenTable.email?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) + }.map { rowToPerson(it) } + } + + private fun rowToPerson(row: ResultRow): Person { + return Person( + id = row[PersonenTable.id], + oepsSatzNr = row[PersonenTable.oepsSatzNr], + nachname = row[PersonenTable.nachname], + vorname = row[PersonenTable.vorname], + titel = row[PersonenTable.titel], + geburtsdatum = row[PersonenTable.geburtsdatum], + geschlechtE = row[PersonenTable.geschlecht], + nationalitaet = row[PersonenTable.nationalitaet], + email = row[PersonenTable.email], + telefon = row[PersonenTable.telefon], + adresse = row[PersonenTable.adresse], + plz = row[PersonenTable.plz], + ort = row[PersonenTable.ort], + stammVereinId = row[PersonenTable.stammVereinId], + mitgliedsNummerIntern = row[PersonenTable.mitgliedsNummerIntern], + letzteZahlungJahr = row[PersonenTable.letzteZahlungJahr], + feiId = row[PersonenTable.feiId], + istGesperrt = row[PersonenTable.istGesperrt], + sperrGrund = row[PersonenTable.sperrGrund], + rollen = parseRollen(row[PersonenTable.rollenCsv]), + lizenzen = emptyList(), // TODO: Load from separate table if needed + qualifikationenRichter = parseQualifikationen(row[PersonenTable.qualifikationenRichterCsv]), + qualifikationenParcoursbauer = parseQualifikationen(row[PersonenTable.qualifikationenParcoursbauerCsv]), + istAktiv = row[PersonenTable.istAktiv], + createdAt = row[PersonenTable.createdAt], + updatedAt = row[PersonenTable.updatedAt] + ) + } + + private fun parseRollen(rollenCsv: String?): Set { + return if (rollenCsv.isNullOrBlank()) { + emptySet() + } else { + rollenCsv.split(",") + .mapNotNull { roleName -> + try { + FunktionaerRolle.valueOf(roleName.trim()) + } catch (e: IllegalArgumentException) { + null + } + } + .toSet() + } + } + + private fun parseQualifikationen(qualifikationenCsv: String?): List { + return if (qualifikationenCsv.isNullOrBlank()) { + emptyList() + } else { + qualifikationenCsv.split(",").map { it.trim() }.filter { it.isNotEmpty() } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt b/server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt new file mode 100644 index 00000000..786f83c0 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/PostgresVereinRepository.kt @@ -0,0 +1,106 @@ +package at.mocode.model + +import at.mocode.shared.stammdaten.Verein +import at.mocode.tables.VereineTable +import com.benasher44.uuid.Uuid +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction + +class PostgresVereinRepository : VereinRepository { + + override suspend fun findAll(): List = transaction { + VereineTable.selectAll().map { rowToVerein(it) } + } + + override suspend fun findById(id: Uuid): Verein? = transaction { + VereineTable.select { VereineTable.id eq id } + .map { rowToVerein(it) } + .singleOrNull() + } + + override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): Verein? = transaction { + VereineTable.select { VereineTable.oepsVereinsNr eq oepsVereinsNr } + .map { rowToVerein(it) } + .singleOrNull() + } + + override suspend fun create(verein: Verein): Verein = transaction { + val now = Clock.System.now() + VereineTable.insert { + it[id] = verein.id + it[oepsVereinsNr] = verein.oepsVereinsNr + it[name] = verein.name + it[kuerzel] = verein.kuerzel + it[bundesland] = verein.bundesland + it[adresse] = verein.adresse + it[plz] = verein.plz + it[ort] = verein.ort + it[email] = verein.email + it[telefon] = verein.telefon + it[webseite] = verein.webseite + it[istAktiv] = verein.istAktiv + it[createdAt] = now + it[updatedAt] = now + } + verein.copy(createdAt = now, updatedAt = now) + } + + override suspend fun update(id: Uuid, verein: Verein): Verein? = transaction { + val updateCount = VereineTable.update({ VereineTable.id eq id }) { + it[name] = verein.name + it[kuerzel] = verein.kuerzel + it[bundesland] = verein.bundesland + it[adresse] = verein.adresse + it[plz] = verein.plz + it[ort] = verein.ort + it[email] = verein.email + it[telefon] = verein.telefon + it[webseite] = verein.webseite + it[istAktiv] = verein.istAktiv + it[updatedAt] = Clock.System.now() + } + if (updateCount > 0) { + VereineTable.select { VereineTable.id eq id } + .map { rowToVerein(it) } + .singleOrNull() + } else null + } + + override suspend fun delete(id: Uuid): Boolean = transaction { + VereineTable.deleteWhere { VereineTable.id eq id } > 0 + } + + override suspend fun findByBundesland(bundesland: String): List = transaction { + VereineTable.select { VereineTable.bundesland eq bundesland } + .map { rowToVerein(it) } + } + + override suspend fun search(query: String): List = transaction { + VereineTable.select { + (VereineTable.name.lowerCase() like "%${query.lowercase()}%") or + (VereineTable.kuerzel?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) or + (VereineTable.ort?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) + }.map { rowToVerein(it) } + } + + private fun rowToVerein(row: ResultRow): Verein { + return Verein( + id = row[VereineTable.id], + oepsVereinsNr = row[VereineTable.oepsVereinsNr], + name = row[VereineTable.name], + kuerzel = row[VereineTable.kuerzel], + bundesland = row[VereineTable.bundesland], + adresse = row[VereineTable.adresse], + plz = row[VereineTable.plz], + ort = row[VereineTable.ort], + email = row[VereineTable.email], + telefon = row[VereineTable.telefon], + webseite = row[VereineTable.webseite], + istAktiv = row[VereineTable.istAktiv], + createdAt = row[VereineTable.createdAt], + updatedAt = row[VereineTable.updatedAt] + ) + } +} diff --git a/server/src/main/kotlin/at/mocode/model/VereinRepository.kt b/server/src/main/kotlin/at/mocode/model/VereinRepository.kt new file mode 100644 index 00000000..67f75260 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/model/VereinRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.model + +import at.mocode.shared.stammdaten.Verein +import com.benasher44.uuid.Uuid + +interface VereinRepository { + suspend fun findAll(): List + suspend fun findById(id: Uuid): Verein? + suspend fun findByOepsVereinsNr(oepsVereinsNr: String): Verein? + suspend fun create(verein: Verein): Verein + suspend fun update(id: Uuid, verein: Verein): Verein? + suspend fun delete(id: Uuid): Boolean + suspend fun findByBundesland(bundesland: String): List + suspend fun search(query: String): List +} diff --git a/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt b/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt new file mode 100644 index 00000000..f9196416 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/ArtikelRoutes.kt @@ -0,0 +1,130 @@ +package at.mocode.routes + +import at.mocode.model.ArtikelRepository +import at.mocode.model.PostgresArtikelRepository +import at.mocode.shared.model.Artikel +import com.benasher44.uuid.uuidFrom +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.artikelRoutes() { + val artikelRepository: ArtikelRepository = PostgresArtikelRepository() + + route("/api/artikel") { + // GET /api/artikel - Get all articles + get { + try { + val artikel = artikelRepository.findAll() + call.respond(HttpStatusCode.OK, artikel) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/artikel/{id} - Get article by ID + get("/{id}") { + try { + val id = call.parameters["id"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing artikel ID") + ) + val uuid = uuidFrom(id) + val artikel = artikelRepository.findById(uuid) + if (artikel != null) { + call.respond(HttpStatusCode.OK, artikel) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Artikel not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/artikel/search?q={query} - Search articles + get("/search") { + try { + val query = call.request.queryParameters["q"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing search query parameter 'q'") + ) + val artikel = artikelRepository.search(query) + call.respond(HttpStatusCode.OK, artikel) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/artikel/verbandsabgabe/{istVerbandsabgabe} - Get articles by association fee status + get("/verbandsabgabe/{istVerbandsabgabe}") { + try { + val istVerbandsabgabe = call.parameters["istVerbandsabgabe"]?.toBoolean() ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing or invalid verbandsabgabe parameter") + ) + val artikel = artikelRepository.findByVerbandsabgabe(istVerbandsabgabe) + call.respond(HttpStatusCode.OK, artikel) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // POST /api/artikel - Create new article + post { + try { + val artikel = call.receive() + val createdArtikel = artikelRepository.create(artikel) + call.respond(HttpStatusCode.Created, createdArtikel) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // PUT /api/artikel/{id} - Update article + put("/{id}") { + try { + val id = call.parameters["id"] ?: return@put call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing artikel ID") + ) + val uuid = uuidFrom(id) + val artikel = call.receive() + val updatedArtikel = artikelRepository.update(uuid, artikel) + if (updatedArtikel != null) { + call.respond(HttpStatusCode.OK, updatedArtikel) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Artikel not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // DELETE /api/artikel/{id} - Delete article + delete("/{id}") { + try { + val id = call.parameters["id"] ?: return@delete call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing artikel ID") + ) + val uuid = uuidFrom(id) + val deleted = artikelRepository.delete(uuid) + if (deleted) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Artikel not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt b/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt new file mode 100644 index 00000000..dd765dca --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/PersonRoutes.kt @@ -0,0 +1,151 @@ +package at.mocode.routes + +import at.mocode.model.PersonRepository +import at.mocode.model.PostgresPersonRepository +import at.mocode.shared.stammdaten.Person +import com.benasher44.uuid.uuidFrom +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.personRoutes() { + val personRepository: PersonRepository = PostgresPersonRepository() + + route("/api/persons") { + // GET /api/persons - Get all persons + get { + try { + val persons = personRepository.findAll() + call.respond(HttpStatusCode.OK, persons) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/persons/{id} - Get person by ID + get("/{id}") { + try { + val id = call.parameters["id"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing person ID") + ) + val uuid = uuidFrom(id) + val person = personRepository.findById(uuid) + if (person != null) { + call.respond(HttpStatusCode.OK, person) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/persons/oeps/{oepsSatzNr} - Get person by OEPS number + get("/oeps/{oepsSatzNr}") { + try { + val oepsSatzNr = call.parameters["oepsSatzNr"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing OEPS Satz number") + ) + val person = personRepository.findByOepsSatzNr(oepsSatzNr) + if (person != null) { + call.respond(HttpStatusCode.OK, person) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found")) + } + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/persons/search?q={query} - Search persons + get("/search") { + try { + val query = call.request.queryParameters["q"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing search query parameter 'q'") + ) + val persons = personRepository.search(query) + call.respond(HttpStatusCode.OK, persons) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/persons/verein/{vereinId} - Get persons by club ID + get("/verein/{vereinId}") { + try { + val vereinId = call.parameters["vereinId"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing verein ID") + ) + val uuid = uuidFrom(vereinId) + val persons = personRepository.findByVereinId(uuid) + call.respond(HttpStatusCode.OK, persons) + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // POST /api/persons - Create new person + post { + try { + val person = call.receive() + val createdPerson = personRepository.create(person) + call.respond(HttpStatusCode.Created, createdPerson) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // PUT /api/persons/{id} - Update person + put("/{id}") { + try { + val id = call.parameters["id"] ?: return@put call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing person ID") + ) + val uuid = uuidFrom(id) + val person = call.receive() + val updatedPerson = personRepository.update(uuid, person) + if (updatedPerson != null) { + call.respond(HttpStatusCode.OK, updatedPerson) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // DELETE /api/persons/{id} - Delete person + delete("/{id}") { + try { + val id = call.parameters["id"] ?: return@delete call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing person ID") + ) + val uuid = uuidFrom(id) + val deleted = personRepository.delete(uuid) + if (deleted) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt new file mode 100644 index 00000000..2a31fd52 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt @@ -0,0 +1,148 @@ +package at.mocode.routes + +import at.mocode.model.PostgresVereinRepository +import at.mocode.model.VereinRepository +import at.mocode.shared.stammdaten.Verein +import com.benasher44.uuid.uuidFrom +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.vereinRoutes() { + val vereinRepository: VereinRepository = PostgresVereinRepository() + + route("/api/vereine") { + // GET /api/vereine - Get all clubs + get { + try { + val vereine = vereinRepository.findAll() + call.respond(HttpStatusCode.OK, vereine) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/vereine/{id} - Get club by ID + get("/{id}") { + try { + val id = call.parameters["id"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing verein ID") + ) + val uuid = uuidFrom(id) + val verein = vereinRepository.findById(uuid) + if (verein != null) { + call.respond(HttpStatusCode.OK, verein) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Verein not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/vereine/oeps/{oepsVereinsNr} - Get club by OEPS number + get("/oeps/{oepsVereinsNr}") { + try { + val oepsVereinsNr = call.parameters["oepsVereinsNr"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing OEPS Vereins number") + ) + val verein = vereinRepository.findByOepsVereinsNr(oepsVereinsNr) + if (verein != null) { + call.respond(HttpStatusCode.OK, verein) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Verein not found")) + } + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/vereine/search?q={query} - Search clubs + get("/search") { + try { + val query = call.request.queryParameters["q"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing search query parameter 'q'") + ) + val vereine = vereinRepository.search(query) + call.respond(HttpStatusCode.OK, vereine) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // GET /api/vereine/bundesland/{bundesland} - Get clubs by state + get("/bundesland/{bundesland}") { + try { + val bundesland = call.parameters["bundesland"] ?: return@get call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing bundesland") + ) + val vereine = vereinRepository.findByBundesland(bundesland) + call.respond(HttpStatusCode.OK, vereine) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + + // POST /api/vereine - Create new club + post { + try { + val verein = call.receive() + val createdVerein = vereinRepository.create(verein) + call.respond(HttpStatusCode.Created, createdVerein) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // PUT /api/vereine/{id} - Update club + put("/{id}") { + try { + val id = call.parameters["id"] ?: return@put call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing verein ID") + ) + val uuid = uuidFrom(id) + val verein = call.receive() + val updatedVerein = vereinRepository.update(uuid, verein) + if (updatedVerein != null) { + call.respond(HttpStatusCode.OK, updatedVerein) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Verein not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) + } + } + + // DELETE /api/vereine/{id} - Delete club + delete("/{id}") { + try { + val id = call.parameters["id"] ?: return@delete call.respond( + HttpStatusCode.BadRequest, + mapOf("error" to "Missing verein ID") + ) + val uuid = uuidFrom(id) + val deleted = vereinRepository.delete(uuid) + if (deleted) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(HttpStatusCode.NotFound, mapOf("error" to "Verein not found")) + } + } catch (e: IllegalArgumentException) { + call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format")) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message)) + } + } + } +}