Compare commits
31 Commits
c7a306e8fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 42398dad21 | |||
|
|
281c25b05d | ||
|
|
1beb5c2eee | ||
|
|
461f2de8dc | ||
|
|
75a7c409cd | ||
|
|
751c302036 | ||
|
|
f0e1ad39b0 | ||
|
|
1dc84023d0 | ||
|
|
30465c7833 | ||
|
|
d260f00756 | ||
|
|
f0ac4b7e56 | ||
|
|
bd45beadd5 | ||
|
|
f060c9771f | ||
| d563d81584 | |||
|
|
0ab71166aa | ||
|
|
d9c5d242ec | ||
| 380d0a8332 | |||
|
|
cb1e7a2fed | ||
|
|
3586704cce | ||
| 301bd7acbd | |||
|
|
7f3b58c5bc | ||
|
|
f29e8a4dac | ||
| 05e25aa3d1 | |||
|
|
b69130c2b2 | ||
|
|
77aabef4a9 | ||
|
|
4b699de853 | ||
|
|
e3cb7282bc | ||
|
|
e537839e28 | ||
|
|
44d441811c | ||
|
|
da1efa5a74 | ||
| ecae88af78 |
6
.gitea/workflows/code-analysis.yml
Normal file → Executable file
6
.gitea/workflows/code-analysis.yml
Normal file → Executable file
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
clean: true
|
||||
fetch-depth: 0
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
echo "eslint entries in package.json:" && rg '"eslint"' package.json || true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
chmod +x osv-scanner
|
||||
./osv-scanner --version
|
||||
test -f ./package-lock.json
|
||||
./osv-scanner --lockfile ./package-lock.json
|
||||
./osv-scanner scan -L ./package-lock.json --config ./.osv-scanner.toml
|
||||
|
||||
deploy-production:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
0
.gitea/workflows/version-gate.yml
Normal file → Executable file
0
.gitea/workflows/version-gate.yml
Normal file → Executable file
6
.github/workflows/android-ci.yml
vendored
Normal file → Executable file
6
.github/workflows/android-ci.yml
vendored
Normal file → Executable file
@@ -10,14 +10,14 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
|
||||
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
7
.gitleaks.toml
Executable file
7
.gitleaks.toml
Executable file
@@ -0,0 +1,7 @@
|
||||
[[allowlists]]
|
||||
description = "generated/imported non-secret data"
|
||||
paths = [
|
||||
'''server/data/spielplan-import/harheimer_tc_spielplan\.(html|json)$''',
|
||||
'''android-app/app/build/.*''',
|
||||
'''android-app/\.idea/planningMode\.xml$''',
|
||||
]
|
||||
0
.gitleaksignore
Normal file → Executable file
0
.gitleaksignore
Normal file → Executable file
4
.osv-scanner.toml
Normal file
4
.osv-scanner.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-v3m3-f69x-jf25"
|
||||
ignoreUntil = 2026-12-31
|
||||
reason = "Temporary exception: Quill 2.0.3 is required by the current RichTextEditor implementation, and OSV currently reports no fixed version. Track upstream fix and remove this ignore once a patched release is available."
|
||||
0
.semgrepignore
Normal file → Executable file
0
.semgrepignore
Normal file → Executable file
0
ANDROID_ARCHITECTURE.md
Normal file → Executable file
0
ANDROID_ARCHITECTURE.md
Normal file → Executable file
0
ANDROID_KOTLIN_PLAN.md
Normal file → Executable file
0
ANDROID_KOTLIN_PLAN.md
Normal file → Executable file
0
ANDROID_PORT_TODO.md
Normal file → Executable file
0
ANDROID_PORT_TODO.md
Normal file → Executable file
0
ANDROID_REPO_ENDPOINTS.md
Normal file → Executable file
0
ANDROID_REPO_ENDPOINTS.md
Normal file → Executable file
0
AUTH_README.md
Normal file → Executable file
0
AUTH_README.md
Normal file → Executable file
0
DATENSCHUTZ.md
Normal file → Executable file
0
DATENSCHUTZ.md
Normal file → Executable file
0
DATENSCHUTZ_UEBERSICHT.md
Normal file → Executable file
0
DATENSCHUTZ_UEBERSICHT.md
Normal file → Executable file
0
DEPLOYMENT.md
Normal file → Executable file
0
DEPLOYMENT.md
Normal file → Executable file
0
android-app/PLAYSTORE_ASSETS.md
Normal file → Executable file
0
android-app/PLAYSTORE_ASSETS.md
Normal file → Executable file
7
android-app/app/build.gradle.kts
Normal file → Executable file
7
android-app/app/build.gradle.kts
Normal file → Executable file
@@ -88,6 +88,13 @@ android {
|
||||
versionName = androidVersionName
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += setOf(
|
||||
"AutoboxingStateCreation",
|
||||
"MutableCollectionMutableState",
|
||||
)
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (hasReleaseSigning) {
|
||||
|
||||
0
android-app/app/instantTest/release/app-instantTest-release.aab
Normal file → Executable file
0
android-app/app/instantTest/release/app-instantTest-release.aab
Normal file → Executable file
0
android-app/app/local/release/app-local-release.aab
Normal file → Executable file
0
android-app/app/local/release/app-local-release.aab
Normal file → Executable file
BIN
android-app/app/production/release/app-production-release.aab
Normal file → Executable file
BIN
android-app/app/production/release/app-production-release.aab
Normal file → Executable file
Binary file not shown.
0
android-app/app/proguard-rules.pro
vendored
Normal file → Executable file
0
android-app/app/proguard-rules.pro
vendored
Normal file → Executable file
0
android-app/app/src/androidTest/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/androidTest/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestBindingsModule.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestBindingsModule.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/TestActivity.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/TestActivity.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsActivateResendTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsActivateResendTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsPasswordResetDiagnosticsScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsPasswordResetDiagnosticsScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsRolesDialogTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsRolesDialogTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsStartseiteSmokeTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsStartseiteSmokeTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsUiAutomatorClickTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsUiAutomatorClickTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/gallery/GalleryScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/gallery/GalleryScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/home/HomeScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/home/HomeScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/login/LoginScreenTest.kt
Normal file → Executable file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/login/LoginScreenTest.kt
Normal file → Executable file
0
android-app/app/src/debug/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/debug/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/main/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/main/AndroidManifest.xml
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/HarheimerApplication.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/HarheimerApplication.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/MainActivity.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/MainActivity.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/AccessTokenAuthenticator.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/AccessTokenAuthenticator.kt
Normal file → Executable file
23
android-app/app/src/main/java/de/harheimertc/data/ApiService.kt
Normal file → Executable file
23
android-app/app/src/main/java/de/harheimertc/data/ApiService.kt
Normal file → Executable file
@@ -21,6 +21,7 @@ import okhttp3.RequestBody
|
||||
data class ContactRequest(val name: String, val email: String, val message: String)
|
||||
data class ContactResponse(val ok: Boolean, val id: String? = null, val message: String? = null)
|
||||
data class TermineResponse(val success: Boolean = true, val termine: List<TerminDto> = emptyList())
|
||||
data class TermineManageResponse(val success: Boolean = true, val termine: List<TerminDto> = emptyList())
|
||||
data class TerminDto(
|
||||
val datum: String = "",
|
||||
val uhrzeit: String? = null,
|
||||
@@ -231,7 +232,7 @@ data class ProfileVisibilityDto(
|
||||
val showEmail: Boolean = true,
|
||||
val showPhone: Boolean = true,
|
||||
val showAddress: Boolean = false,
|
||||
val showBirthday: Boolean = true,
|
||||
val showBirthday: Boolean = false,
|
||||
)
|
||||
data class ProfileUserDto(
|
||||
val id: String? = null,
|
||||
@@ -328,6 +329,7 @@ data class MemberDto(
|
||||
val editable: Boolean = false,
|
||||
val isMannschaftsspieler: Boolean = false,
|
||||
val hasHallKey: Boolean = false,
|
||||
val showBirthday: Boolean = false,
|
||||
val loginRoles: List<String> = emptyList(),
|
||||
)
|
||||
data class MembersResponse(
|
||||
@@ -590,6 +592,21 @@ interface ApiService {
|
||||
@GET("/api/termine")
|
||||
suspend fun termine(): Response<TermineResponse>
|
||||
|
||||
@GET("/api/termine-manage")
|
||||
suspend fun termineManage(): Response<TermineManageResponse>
|
||||
|
||||
@POST("/api/termine-manage")
|
||||
suspend fun saveTermin(@Body request: TerminDto): Response<AuthMessageResponse>
|
||||
|
||||
@DELETE("/api/termine-manage")
|
||||
suspend fun deleteTermin(
|
||||
@Query("datum") datum: String,
|
||||
@Query("uhrzeit") uhrzeit: String = "",
|
||||
@Query("titel") titel: String,
|
||||
@Query("beschreibung") beschreibung: String = "",
|
||||
@Query("kategorie") kategorie: String = "Sonstiges",
|
||||
): Response<AuthMessageResponse>
|
||||
|
||||
@GET("/api/spielplan")
|
||||
suspend fun spielplan(@Query("season") season: String? = null): Response<SpielplanResponse>
|
||||
|
||||
@@ -635,6 +652,9 @@ interface ApiService {
|
||||
@POST("/api/cms/save-csv")
|
||||
suspend fun saveCsv(@Body request: SaveCsvRequest): Response<SaveCsvResponse>
|
||||
|
||||
@POST("/api/cms/import-spielplan")
|
||||
suspend fun importSpielplan(): Response<AuthMessageResponse>
|
||||
|
||||
@POST("/api/membership/generate-pdf")
|
||||
suspend fun generateMembershipPdf(@Body request: MembershipRequest): Response<MembershipResponse>
|
||||
|
||||
@@ -713,6 +733,7 @@ interface ApiService {
|
||||
val notes: String? = null,
|
||||
val isMannschaftsspieler: Boolean = false,
|
||||
val hasHallKey: Boolean = false,
|
||||
val showBirthday: Boolean = false,
|
||||
)
|
||||
|
||||
data class BulkImportRequest(val members: List<Map<String, String>>)
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/data/AuthInterceptor.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/AuthInterceptor.kt
Normal file → Executable file
4
android-app/app/src/main/java/de/harheimertc/data/ConnectivityMonitor.kt
Normal file → Executable file
4
android-app/app/src/main/java/de/harheimertc/data/ConnectivityMonitor.kt
Normal file → Executable file
@@ -19,7 +19,7 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ConnectivityMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@param:ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val _online = MutableStateFlow(hasInternetAccess())
|
||||
@@ -46,4 +46,4 @@ class ConnectivityMonitor @Inject constructor(
|
||||
val capabilities = manager.getNetworkCapabilities(network) ?: return false
|
||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/data/MediaTypes.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/MediaTypes.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/NetworkModule.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/NetworkModule.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/SecureOfflineCache.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/SecureOfflineCache.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/SessionRefresher.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/data/SessionRefresher.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/di/RepositoryModule.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/di/RepositoryModule.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt
Normal file → Executable file
15
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerNotifications.kt
Normal file → Executable file
15
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerNotifications.kt
Normal file → Executable file
@@ -51,8 +51,12 @@ object HarheimerNotifications {
|
||||
.setContentIntent(createContentIntent(context, notificationId, data))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
||||
return true
|
||||
return try {
|
||||
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
||||
true
|
||||
} catch (_: SecurityException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createContentIntent(context: Context, notificationId: Int, payload: Map<String, String>): PendingIntent {
|
||||
@@ -72,7 +76,12 @@ object HarheimerNotifications {
|
||||
}
|
||||
|
||||
private fun destinationRoute(data: Map<String, String>): String = when (data["type"]) {
|
||||
"news" -> Destinations.MemberNews.route
|
||||
"news", "news_expiring" -> Destinations.MemberNews.route
|
||||
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
|
||||
"team_matches" -> Destinations.Spielplan.route
|
||||
"birthdays" -> Destinations.MemberArea.route
|
||||
"contact_request" -> Destinations.CmsContactRequests.route
|
||||
"user_registration" -> Destinations.CmsBenutzer.route
|
||||
else -> Destinations.Home.route
|
||||
}
|
||||
}
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepositoryImpl.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepositoryImpl.kt
Normal file → Executable file
144
android-app/app/src/main/java/de/harheimertc/repositories/CmsRepository.kt
Normal file → Executable file
144
android-app/app/src/main/java/de/harheimertc/repositories/CmsRepository.kt
Normal file → Executable file
@@ -10,6 +10,8 @@ import de.harheimertc.data.PasswordResetDiagnosticsResponse
|
||||
import de.harheimertc.data.SaveCsvRequest
|
||||
import de.harheimertc.data.SaveCsvResponse
|
||||
import de.harheimertc.data.SecureOfflineCache
|
||||
import de.harheimertc.data.SpielplanResponse
|
||||
import de.harheimertc.data.TerminDto
|
||||
import javax.inject.Inject
|
||||
|
||||
class CmsRepository @Inject constructor(
|
||||
@@ -83,6 +85,93 @@ class CmsRepository @Inject constructor(
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun managedTermine(): Result<List<TerminDto>> = runCatching {
|
||||
val response = api.termineManage()
|
||||
if (!response.isSuccessful) error("Termine konnten nicht geladen werden.")
|
||||
response.body()?.termine.orEmpty()
|
||||
}
|
||||
|
||||
suspend fun saveTermin(request: TerminDto): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.saveTermin(request)
|
||||
if (!response.isSuccessful) error("Termin konnte nicht gespeichert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun deleteTermin(request: TerminDto): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.deleteTermin(
|
||||
datum = request.datum,
|
||||
uhrzeit = request.uhrzeit.orEmpty(),
|
||||
titel = request.titel,
|
||||
beschreibung = request.beschreibung.orEmpty(),
|
||||
kategorie = request.kategorie ?: "Sonstiges",
|
||||
)
|
||||
if (!response.isSuccessful) error("Termin konnte nicht gelöscht werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun mannschaften(season: String? = null): Result<List<CmsMannschaftRow>> = runCatching {
|
||||
val response = api.mannschaften(season)
|
||||
if (!response.isSuccessful) error("Mannschaften konnten nicht geladen werden.")
|
||||
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
||||
if (values.size < 10 || values[0].isBlank()) return@mapNotNull null
|
||||
CmsMannschaftRow(
|
||||
mannschaft = values[0],
|
||||
liga = values[1],
|
||||
staffelleiter = values[2],
|
||||
telefon = values[3],
|
||||
heimspieltag = values[4],
|
||||
spielsystem = values[5],
|
||||
mannschaftsfuehrer = values[6],
|
||||
spieler = values[7],
|
||||
informationenLink = values[8],
|
||||
letzteAktualisierung = values[9],
|
||||
spielplanMannschaftId = values.getOrElse(10) { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun mannschaftenSeasons(): Result<de.harheimertc.data.MannschaftenSeasonsResponse> = runCatching {
|
||||
val response = api.mannschaftenSeasons()
|
||||
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
|
||||
response.body() ?: de.harheimertc.data.MannschaftenSeasonsResponse()
|
||||
}
|
||||
|
||||
suspend fun saveMannschaften(season: String?, rows: List<CmsMannschaftRow>): Result<SaveCsvResponse> = runCatching {
|
||||
val response = api.saveCsv(
|
||||
SaveCsvRequest(
|
||||
filename = season?.takeIf { it.isNotBlank() }?.let { "mannschaften_$it.csv" } ?: "mannschaften.csv",
|
||||
content = rows.toMannschaftenCsv(),
|
||||
),
|
||||
)
|
||||
if (!response.isSuccessful) error("Mannschaften konnten nicht gespeichert werden.")
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun importSpielplan(): Result<de.harheimertc.data.AuthMessageResponse> = runCatching {
|
||||
val response = api.importSpielplan()
|
||||
if (!response.isSuccessful) error("Spielplan konnte nicht importiert werden.")
|
||||
response.body() ?: de.harheimertc.data.AuthMessageResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun spielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
|
||||
val response = api.spielplan(season)
|
||||
if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.")
|
||||
val body = response.body() ?: error("Leere Antwort")
|
||||
if (!body.success) error(body.message ?: "Spielplan konnte nicht geladen werden.")
|
||||
body
|
||||
}
|
||||
|
||||
suspend fun saveSpielplan(headers: List<String>, rows: List<List<String>>): Result<SaveCsvResponse> = runCatching {
|
||||
val response = api.saveCsv(
|
||||
SaveCsvRequest(
|
||||
filename = "spielplan.csv",
|
||||
content = listOf(headers).plus(rows).joinToString("\n") { row -> row.toCsvRow(";") },
|
||||
),
|
||||
)
|
||||
if (!response.isSuccessful) error("Spielplan konnte nicht gespeichert werden.")
|
||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
||||
}
|
||||
|
||||
suspend fun users(): Result<CmsUsersResponse> =
|
||||
fetchEncryptedFallback(
|
||||
load = {
|
||||
@@ -288,6 +377,61 @@ class CmsRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
data class CmsMannschaftRow(
|
||||
val mannschaft: String = "",
|
||||
val liga: String = "",
|
||||
val staffelleiter: String = "",
|
||||
val telefon: String = "",
|
||||
val heimspieltag: String = "",
|
||||
val spielsystem: String = "",
|
||||
val mannschaftsfuehrer: String = "",
|
||||
val spieler: String = "",
|
||||
val informationenLink: String = "",
|
||||
val letzteAktualisierung: String = "",
|
||||
val spielplanMannschaftId: String = "",
|
||||
)
|
||||
|
||||
private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
||||
val header = listOf(
|
||||
"Mannschaft",
|
||||
"Liga",
|
||||
"Staffelleiter",
|
||||
"Telefon",
|
||||
"Heimspieltag",
|
||||
"Spielsystem",
|
||||
"Mannschaftsführer",
|
||||
"Spieler",
|
||||
"Weitere Informationen Link",
|
||||
"Letzte Aktualisierung",
|
||||
"Spielplan Mannschaft ID",
|
||||
).toCsvRow()
|
||||
val rows = map { row ->
|
||||
listOf(
|
||||
row.mannschaft,
|
||||
row.liga,
|
||||
row.staffelleiter,
|
||||
row.telefon,
|
||||
row.heimspieltag,
|
||||
row.spielsystem,
|
||||
row.mannschaftsfuehrer,
|
||||
row.spieler,
|
||||
row.informationenLink,
|
||||
row.letzteAktualisierung,
|
||||
row.spielplanMannschaftId,
|
||||
).toCsvRow()
|
||||
}
|
||||
return listOf(header).plus(rows).joinToString("\n")
|
||||
}
|
||||
|
||||
private fun List<String>.toCsvRow(delimiter: String = ","): String =
|
||||
joinToString(delimiter) { value -> value.csvEscape(delimiter) }
|
||||
|
||||
private fun String.csvEscape(delimiter: String): String {
|
||||
val needsQuotes = contains(delimiter) || contains('"') || contains('\n') || contains('\r')
|
||||
val escaped = replace("\"", "\"\"")
|
||||
return if (needsQuotes) "\"$escaped\"" else escaped
|
||||
}
|
||||
|
||||
private fun parseCsv(csv: String): List<List<String>> =
|
||||
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
||||
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/repositories/ContactRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/ContactRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/GalleryRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/GalleryRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeLayoutPreferences.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeLayoutPreferences.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/LoginRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/LoginRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MannschaftenRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MannschaftenRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MemberAreaRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MemberAreaRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MembershipRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/MembershipRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NetworkRetry.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NetworkRetry.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NewsletterRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NewsletterRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NotificationPreferencesRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/NotificationPreferencesRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PasskeyRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PasskeyRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/ProfileRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/ProfileRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PublicPagesRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PublicPagesRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/SpielplanRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/SpielplanRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/TermineRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/TermineRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/TrainingRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/repositories/TrainingRepository.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/security/DeviceKeyManager.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/security/DeviceKeyManager.kt
Normal file → Executable file
3
android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt
Normal file → Executable file
3
android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt
Normal file → Executable file
@@ -422,6 +422,7 @@ private fun menuSection(route: String?): MenuSection? = when (route) {
|
||||
Destinations.CmsStartseite.route,
|
||||
Destinations.CmsInhalte.route,
|
||||
Destinations.CmsVereinsmeisterschaften.route,
|
||||
Destinations.CmsNews.route,
|
||||
Destinations.CmsSportbetrieb.route,
|
||||
Destinations.CmsMitgliederverwaltung.route,
|
||||
Destinations.CmsNewsletter.route,
|
||||
@@ -484,7 +485,7 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
||||
add(MenuTarget("Startseite", Destinations.CmsStartseite.route))
|
||||
add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
||||
add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.route))
|
||||
add(MenuTarget("News", Destinations.MemberNews.route))
|
||||
add(MenuTarget("News", Destinations.CmsNews.route))
|
||||
add(MenuTarget("Sportbetrieb", Destinations.CmsSportbetrieb.route))
|
||||
add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
|
||||
add(MenuTarget("Einstellungen", Destinations.CmsEinstellungen.route))
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/ui/components/FormComponents.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/FormComponents.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/HeroComponent.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/HeroComponent.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/ImageGrid.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/ImageGrid.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/LoadingState.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/LoadingState.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/NativeRichTextEditor.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/NativeRichTextEditor.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/PendingPage.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/PendingPage.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichText.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichText.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichTextUtils.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichTextUtils.kt
Normal file → Executable file
1
android-app/app/src/main/java/de/harheimertc/ui/navigation/Destinations.kt
Normal file → Executable file
1
android-app/app/src/main/java/de/harheimertc/ui/navigation/Destinations.kt
Normal file → Executable file
@@ -48,6 +48,7 @@ sealed class Destinations(val route: String) {
|
||||
object CmsStartseite : Destinations("cms/startseite")
|
||||
object CmsInhalte : Destinations("cms/inhalte")
|
||||
object CmsVereinsmeisterschaften : Destinations("cms/vereinsmeisterschaften")
|
||||
object CmsNews : Destinations("cms/news")
|
||||
object CmsSportbetrieb : Destinations("cms/sportbetrieb")
|
||||
object CmsMitgliederverwaltung : Destinations("cms/mitgliederverwaltung")
|
||||
object CmsNewsletter : Destinations("cms/newsletter")
|
||||
|
||||
123
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavGraph.kt
Normal file → Executable file
123
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavGraph.kt
Normal file → Executable file
@@ -2,7 +2,9 @@ package de.harheimertc.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -327,43 +330,141 @@ fun NavGraph(
|
||||
)
|
||||
}
|
||||
composable(Destinations.CmsStartseite.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsStartseiteScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsStartseiteScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsInhalte.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsInhalteScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsInhalteScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsVereinsmeisterschaften.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsVereinsmeisterschaftenScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsVereinsmeisterschaftenScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsNews.route) {
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsNewsScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsSportbetrieb.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsSportbetriebScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsSportbetriebScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsMitgliederverwaltung.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsMitgliederverwaltungScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsMitgliederverwaltungScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsNewsletter.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsNewsletterScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessNewsletter,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsNewsletterScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsContactRequests.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsContactRequestsScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessContactRequests,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsContactRequestsScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsEinstellungen.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsEinstellungenScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsEinstellungenScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsBenutzer.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsBenutzerScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsBenutzerScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.CmsPasswordResetDiagnostics.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsPasswordResetDiagnosticsScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessFullCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsPasswordResetDiagnosticsScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
composable(Destinations.Cms.route) {
|
||||
de.harheimertc.ui.screens.cms.CmsDashboardScreen(navController, !persistentNavigation)
|
||||
RequireCmsAccess(
|
||||
allowed = navigationState.canAccessCms,
|
||||
navController = navController,
|
||||
) {
|
||||
de.harheimertc.ui.screens.cms.CmsDashboardScreen(navController, !persistentNavigation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequireCmsAccess(
|
||||
allowed: Boolean,
|
||||
navController: NavHostController,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
if (allowed) {
|
||||
content()
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "Kein Zugriff auf diesen Bereich.",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color(0xFF7F1D1D),
|
||||
)
|
||||
Text(
|
||||
text = "Bitte mit einem berechtigten Konto anmelden.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF7C2D12),
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
|
||||
)
|
||||
Button(onClick = { navController.navigateTopLevel(Destinations.Login.route) }) {
|
||||
Text("Zum Login")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavHostController.navigateTopLevel(route: String) {
|
||||
val isTeamDetail = route.startsWith("mannschaften/") &&
|
||||
route != Destinations.Spielsysteme.route
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavigationViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavigationViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsBenutzerScreens.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsBenutzerScreens.kt
Normal file → Executable file
34
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsNewsScreens.kt
Normal file → Executable file
34
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsNewsScreens.kt
Normal file → Executable file
@@ -132,15 +132,22 @@ fun CmsNewsScreen(navController: NavController, showBackNavigation: Boolean, vie
|
||||
|
||||
items(state.news) { news ->
|
||||
val selected = news.id?.let { selection.contains(it) } ?: false
|
||||
NewsListItem(news = news, selected = selected, onSelect = { id, sel ->
|
||||
id?.let {
|
||||
selection = if (sel) selection + it else selection - it
|
||||
}
|
||||
}, onEdit = { openEdit(news) }, onDelete = { news.id?.let { id -> deletingIds = listOf(id) } })
|
||||
NewsListItem(
|
||||
news = news,
|
||||
canWrite = canWrite,
|
||||
selected = selected,
|
||||
onSelect = { id, sel ->
|
||||
id?.let {
|
||||
selection = if (sel) selection + it else selection - it
|
||||
}
|
||||
},
|
||||
onEdit = { openEdit(news) },
|
||||
onDelete = { news.id?.let { id -> deletingIds = listOf(id) } },
|
||||
)
|
||||
}
|
||||
|
||||
// bulk action bar
|
||||
if (selection.isNotEmpty()) {
|
||||
if (canWrite && selection.isNotEmpty()) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.bulkSetPublic(selection.toList(), true) }) { Text("Als öffentlich markieren") }
|
||||
@@ -255,6 +262,7 @@ fun CmsNewsScreen(navController: NavController, showBackNavigation: Boolean, vie
|
||||
@Composable
|
||||
private fun NewsListItem(
|
||||
news: NewsDto,
|
||||
canWrite: Boolean,
|
||||
selected: Boolean = false,
|
||||
onSelect: (String?, Boolean) -> Unit = { _, _ -> },
|
||||
onEdit: (NewsDto) -> Unit,
|
||||
@@ -264,7 +272,11 @@ private fun NewsListItem(
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Checkbox(checked = selected, onCheckedChange = { onSelect(news.id, it) })
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { onSelect(news.id, it) },
|
||||
enabled = canWrite,
|
||||
)
|
||||
Text(news.title.ifBlank { "(Ohne Titel)" }, modifier = Modifier.padding(start = 8.dp))
|
||||
if (news.isPublic) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
||||
@@ -297,9 +309,11 @@ private fun NewsListItem(
|
||||
Text("Aktualisiert: ${news.updated}")
|
||||
}
|
||||
}
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
if (canWrite) {
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
652
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsScreens.kt
Normal file → Executable file
652
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsScreens.kt
Normal file → Executable file
@@ -1,8 +1,10 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import android.content.Intent
|
||||
import android.app.DatePickerDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -36,6 +38,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.navigation.NavController
|
||||
@@ -48,10 +52,12 @@ import de.harheimertc.data.NewsletterGroupDto
|
||||
import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||
import de.harheimertc.data.PasswordResetAttemptDto
|
||||
import de.harheimertc.data.PasswordResetStepDto
|
||||
import de.harheimertc.data.TerminDto
|
||||
import de.harheimertc.ui.components.FormMessages
|
||||
import de.harheimertc.ui.components.LoadingState
|
||||
import de.harheimertc.ui.components.NativeRichTextEditor
|
||||
import de.harheimertc.ui.navigation.Destinations
|
||||
import de.harheimertc.repositories.CmsMannschaftRow
|
||||
import de.harheimertc.repositories.MeisterschaftResult
|
||||
import de.harheimertc.ui.theme.Accent100
|
||||
import de.harheimertc.ui.theme.Accent500
|
||||
@@ -61,6 +67,7 @@ import de.harheimertc.ui.theme.Primary100
|
||||
import de.harheimertc.ui.theme.Primary600
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@@ -433,116 +440,426 @@ fun CmsVereinsmeisterschaftenScreen(navController: NavController, showBackNaviga
|
||||
@Composable
|
||||
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val config = state.config
|
||||
var ortName by remember { mutableStateOf("") }
|
||||
var ortStrasse by remember { mutableStateOf("") }
|
||||
var ortPlz by remember { mutableStateOf("") }
|
||||
var ortOrt by remember { mutableStateOf("") }
|
||||
val trainingTimes = remember { mutableStateListOf<de.harheimertc.data.TrainingTimeDto>() }
|
||||
val trainers = remember { mutableStateListOf<de.harheimertc.data.TrainerDto>() }
|
||||
val context = LocalContext.current
|
||||
var activeTab by remember { mutableStateOf("termine") }
|
||||
val mannschaften = remember { mutableStateListOf<CmsMannschaftRow>() }
|
||||
var spielplanCsv by remember { mutableStateOf("") }
|
||||
var spielplanEditorOpen by remember { mutableStateOf(false) }
|
||||
var terminDialogOpen by remember { mutableStateOf(false) }
|
||||
var editingTermin by remember { mutableStateOf<TerminDto?>(null) }
|
||||
var terminDatum by remember { mutableStateOf("") }
|
||||
var terminUhrzeit by remember { mutableStateOf("") }
|
||||
var terminTitel by remember { mutableStateOf("") }
|
||||
var terminBeschreibung by remember { mutableStateOf("") }
|
||||
var terminKategorie by remember { mutableStateOf("Sonstiges") }
|
||||
var terminKategorieOpen by remember { mutableStateOf(false) }
|
||||
val tabs = listOf(
|
||||
"termine" to "Termine",
|
||||
"mannschaften" to "Mannschaften",
|
||||
"spielplaene" to "Spielpläne",
|
||||
)
|
||||
val spielplanTeamOptions = remember(state.sportSpielplanHeaders, state.sportSpielplanRows) {
|
||||
importedSpielplanTeams(state.sportSpielplanHeaders, state.sportSpielplanRows)
|
||||
}
|
||||
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
|
||||
|
||||
LaunchedEffect(config) {
|
||||
config?.let {
|
||||
ortName = it.training.ort.name
|
||||
ortStrasse = it.training.ort.strasse
|
||||
ortPlz = it.training.ort.plz
|
||||
ortOrt = it.training.ort.ort
|
||||
trainingTimes.clear()
|
||||
trainingTimes.addAll(it.training.zeiten)
|
||||
trainers.clear()
|
||||
trainers.addAll(it.trainer)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadSportbetrieb()
|
||||
}
|
||||
|
||||
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Trainingszeiten, Trainingsort und Trainer pflegen") {
|
||||
when {
|
||||
state.loading || config == null -> item { LoadingState("Sportbetriebsdaten werden geladen...") }
|
||||
else -> {
|
||||
item {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.saveConfig(
|
||||
config.copy(
|
||||
training = config.training.copy(
|
||||
ort = config.training.ort.copy(
|
||||
name = ortName,
|
||||
strasse = ortStrasse,
|
||||
plz = ortPlz,
|
||||
ort = ortOrt,
|
||||
),
|
||||
zeiten = trainingTimes.toList(),
|
||||
),
|
||||
trainer = trainers.toList(),
|
||||
),
|
||||
)
|
||||
},
|
||||
enabled = !state.saving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(if (state.saving) "Speichert..." else "Speichern")
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingsort") {
|
||||
OutlinedTextField(value = ortName, onValueChange = { ortName = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = ortStrasse, onValueChange = { ortStrasse = it }, label = { Text("Straße") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(value = ortPlz, onValueChange = { ortPlz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(value = ortOrt, onValueChange = { ortOrt = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingszeiten") {
|
||||
trainingTimes.forEachIndexed { index, zeit ->
|
||||
TrainingTimeEditorCard(
|
||||
zeit = zeit,
|
||||
onChange = { updated -> trainingTimes[index] = updated },
|
||||
onRemove = { trainingTimes.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainingTimes.add(
|
||||
de.harheimertc.data.TrainingTimeDto(
|
||||
id = "training-${System.currentTimeMillis()}",
|
||||
tag = "Montag",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainingszeit hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainer") {
|
||||
trainers.forEachIndexed { index, trainer ->
|
||||
TrainerEditorCard(
|
||||
trainer = trainer,
|
||||
onChange = { updated -> trainers[index] = updated },
|
||||
onRemove = { trainers.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainers.add(
|
||||
de.harheimertc.data.TrainerDto(
|
||||
id = "trainer-${System.currentTimeMillis()}",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainer hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item { FormMessages(state.error, state.message) }
|
||||
LaunchedEffect(state.sportMannschaften) {
|
||||
mannschaften.clear()
|
||||
mannschaften.addAll(state.sportMannschaften)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.sportSpielplanHeaders, state.sportSpielplanRows) {
|
||||
spielplanCsv = sportSpielplanCsvText(state.sportSpielplanHeaders, state.sportSpielplanRows)
|
||||
}
|
||||
|
||||
fun openTerminDialog(termin: TerminDto?) {
|
||||
editingTermin = termin
|
||||
terminDatum = termin?.datum.orEmpty()
|
||||
terminUhrzeit = termin?.uhrzeit.orEmpty()
|
||||
terminTitel = termin?.titel.orEmpty()
|
||||
terminBeschreibung = termin?.beschreibung.orEmpty()
|
||||
terminKategorie = termin?.kategorie ?: "Sonstiges"
|
||||
terminDialogOpen = true
|
||||
}
|
||||
|
||||
fun openDatePicker() {
|
||||
val calendar = Calendar.getInstance()
|
||||
runCatching {
|
||||
val parts = terminDatum.split("-")
|
||||
if (parts.size == 3) {
|
||||
calendar.set(parts[0].toInt(), parts[1].toInt() - 1, parts[2].toInt())
|
||||
}
|
||||
}
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _, year, month, day ->
|
||||
terminDatum = "%04d-%02d-%02d".format(Locale.ROOT, year, month + 1, day)
|
||||
},
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH),
|
||||
).show()
|
||||
}
|
||||
|
||||
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Termine, Mannschaften und Spielpläne") {
|
||||
if (state.sportLoading) item { LoadingState("Sportbetriebsdaten werden geladen...") }
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
tabs.forEach { (id, label) ->
|
||||
if (activeTab == id) {
|
||||
Button(onClick = { activeTab = id }, modifier = Modifier.weight(1f)) { Text(label) }
|
||||
} else {
|
||||
OutlinedButton(onClick = { activeTab = id }, modifier = Modifier.weight(1f)) { Text(label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.sportLoading) {
|
||||
when (activeTab) {
|
||||
"termine" -> {
|
||||
item {
|
||||
Button(
|
||||
onClick = { openTerminDialog(null) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Termin hinzufügen")
|
||||
}
|
||||
}
|
||||
if (state.sportTermine.isEmpty()) {
|
||||
item { EmptyCard("Keine Termine gefunden.") }
|
||||
}
|
||||
items(state.sportTermine.size) { index ->
|
||||
val termin = state.sportTermine[index]
|
||||
DataCard(termin.titel.ifBlank { "Termin" }) {
|
||||
InfoRow("Datum", listOf(termin.datum, termin.uhrzeit.orEmpty()).filter(String::isNotBlank).joinToString(" "))
|
||||
InfoRow("Kategorie", termin.kategorie ?: "Sonstiges")
|
||||
if (!termin.beschreibung.isNullOrBlank()) {
|
||||
Text(termin.beschreibung, color = Accent700)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { openTerminDialog(termin) }, modifier = Modifier.weight(1f)) {
|
||||
Text("Bearbeiten")
|
||||
}
|
||||
TextButton(
|
||||
onClick = { viewModel.deleteSportTermin(termin) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Löschen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"mannschaften" -> {
|
||||
item {
|
||||
if (state.sportMannschaftenSeasons.isNotEmpty()) {
|
||||
DataCard("Saison") {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
state.sportMannschaftenSeasons.forEach { season ->
|
||||
if (season == state.sportMannschaftenSeason) {
|
||||
Button(onClick = { }, modifier = Modifier.weight(1f)) { Text(season) }
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.loadSportMannschaftenSeason(season) },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(season)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = {
|
||||
mannschaften.add(CmsMannschaftRow(letzteAktualisierung = OffsetDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)))
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Hinzufügen")
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.saveSportMannschaften(state.sportMannschaftenSeason, mannschaften.toList()) },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mannschaften.isEmpty()) {
|
||||
item { EmptyCard("Keine Mannschaften gefunden.") }
|
||||
}
|
||||
items(mannschaften.size) { index ->
|
||||
MannschaftEditorCard(
|
||||
row = mannschaften[index],
|
||||
spielplanTeams = spielplanTeamOptions,
|
||||
onChange = { updated -> mannschaften[index] = updated },
|
||||
onRemove = { mannschaften.removeAt(index) },
|
||||
)
|
||||
}
|
||||
}
|
||||
"spielplaene" -> {
|
||||
item {
|
||||
DataCard("Vereins-Spielplan (CSV)") {
|
||||
val seasonLabel = state.sportSpielplanSeason.ifBlank { "aktuelle Saison" }
|
||||
val fileName = state.sportSpielplanSeason.takeIf { it.isNotBlank() }?.let { "spielplan-$it.json" } ?: "spielplan.csv"
|
||||
InfoRow("Datei", fileName)
|
||||
InfoRow("Saison", seasonLabel)
|
||||
InfoRow("Einträge", state.sportSpielplanRows.size.toString())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = { viewModel.importSportSpielplan() },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Importiert..." else "Von myTischtennis importieren")
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.loadSportbetrieb() },
|
||||
enabled = !state.sportSaving,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Neu laden")
|
||||
}
|
||||
Button(
|
||||
onClick = { spielplanEditorOpen = true },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("CSV bearbeiten")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item { FormMessages(state.error, state.message) }
|
||||
}
|
||||
}
|
||||
|
||||
if (terminDialogOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { terminDialogOpen = false },
|
||||
title = { Text(if (editingTermin == null) "Termin hinzufügen" else "Termin bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = { openDatePicker() }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(terminDatum.ifBlank { "Datum auswählen" })
|
||||
}
|
||||
OutlinedTextField(value = terminUhrzeit, onValueChange = { terminUhrzeit = it }, label = { Text("Uhrzeit") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = terminTitel, onValueChange = { terminTitel = it }, label = { Text("Titel") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = terminBeschreibung, onValueChange = { terminBeschreibung = it }, label = { Text("Beschreibung") }, modifier = Modifier.fillMaxWidth(), minLines = 3)
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { terminKategorieOpen = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(terminKategorie.ifBlank { "Kategorie auswählen" })
|
||||
}
|
||||
DropdownMenu(expanded = terminKategorieOpen, onDismissRequest = { terminKategorieOpen = false }) {
|
||||
terminKategorien.forEach { kategorie ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(kategorie) },
|
||||
onClick = {
|
||||
terminKategorie = kategorie
|
||||
terminKategorieOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.saveSportTermin(
|
||||
editingTermin,
|
||||
TerminDto(
|
||||
datum = terminDatum,
|
||||
uhrzeit = terminUhrzeit.takeIf { it.isNotBlank() },
|
||||
titel = terminTitel,
|
||||
beschreibung = terminBeschreibung.takeIf { it.isNotBlank() },
|
||||
kategorie = terminKategorie.ifBlank { "Sonstiges" },
|
||||
),
|
||||
)
|
||||
terminDialogOpen = false
|
||||
},
|
||||
enabled = !state.sportSaving && terminDatum.isNotBlank() && terminTitel.isNotBlank(),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { terminDialogOpen = false }) { Text("Abbrechen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (spielplanEditorOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { spielplanEditorOpen = false },
|
||||
title = { Text("Spielplan CSV bearbeiten") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = spielplanCsv,
|
||||
onValueChange = { spielplanCsv = it },
|
||||
label = { Text("CSV mit Semikolon") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 12,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
val (headers, rows) = parseSportCsvText(spielplanCsv)
|
||||
viewModel.saveSportSpielplan(headers, rows)
|
||||
spielplanEditorOpen = false
|
||||
},
|
||||
enabled = !state.sportSaving && spielplanCsv.isNotBlank(),
|
||||
) {
|
||||
Text(if (state.sportSaving) "Speichert..." else "Speichern")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { spielplanEditorOpen = false }) { Text("Abbrechen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MannschaftEditorCard(
|
||||
row: CmsMannschaftRow,
|
||||
spielplanTeams: List<ImportedSpielplanTeam>,
|
||||
onChange: (CmsMannschaftRow) -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
) {
|
||||
var teamPickerOpen by remember(row.mannschaft, spielplanTeams) { mutableStateOf(false) }
|
||||
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
|
||||
OutlinedTextField(value = row.mannschaft, onValueChange = { onChange(row.copy(mannschaft = it)) }, label = { Text("Mannschaft") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.spielplanMannschaftId, onValueChange = { onChange(row.copy(spielplanMannschaftId = it)) }, label = { Text("Spielplan-Team-ID") }, supportingText = { Text("Eindeutige myTischtennis-Team-ID") }, modifier = Modifier.fillMaxWidth())
|
||||
if (spielplanTeams.isNotEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(onClick = { teamPickerOpen = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Aus importierten Teams auswählen")
|
||||
}
|
||||
DropdownMenu(expanded = teamPickerOpen, onDismissRequest = { teamPickerOpen = false }) {
|
||||
spielplanTeams.forEach { team ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(team.label) },
|
||||
onClick = {
|
||||
onChange(row.copy(spielplanMannschaftId = team.id))
|
||||
teamPickerOpen = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(value = row.liga, onValueChange = { onChange(row.copy(liga = it)) }, label = { Text("Liga") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.staffelleiter, onValueChange = { onChange(row.copy(staffelleiter = it)) }, label = { Text("Staffelleiter") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.telefon, onValueChange = { onChange(row.copy(telefon = it)) }, label = { Text("Telefon") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.heimspieltag, onValueChange = { onChange(row.copy(heimspieltag = it)) }, label = { Text("Heimspieltag") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.spielsystem, onValueChange = { onChange(row.copy(spielsystem = it)) }, label = { Text("Spielsystem") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.mannschaftsfuehrer, onValueChange = { onChange(row.copy(mannschaftsfuehrer = it)) }, label = { Text("Mannschaftsführer") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.spieler, onValueChange = { onChange(row.copy(spieler = it)) }, label = { Text("Spieler") }, modifier = Modifier.fillMaxWidth(), minLines = 2)
|
||||
OutlinedTextField(value = row.informationenLink, onValueChange = { onChange(row.copy(informationenLink = it)) }, label = { Text("Weitere Informationen Link") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = row.letzteAktualisierung, onValueChange = { onChange(row.copy(letzteAktualisierung = it)) }, label = { Text("Letzte Aktualisierung") }, modifier = Modifier.fillMaxWidth())
|
||||
TextButton(onClick = onRemove, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Entfernen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ImportedSpielplanTeam(val id: String, val label: String)
|
||||
|
||||
private fun importedSpielplanTeams(headers: List<String>, rows: List<List<String>>): List<ImportedSpielplanTeam> {
|
||||
fun index(name: String) = headers.indexOf(name)
|
||||
fun value(row: List<String>, column: String): String {
|
||||
val columnIndex = index(column)
|
||||
return row.getOrElse(columnIndex) { "" }.trim()
|
||||
}
|
||||
|
||||
val teams = linkedMapOf<String, ImportedSpielplanTeam>()
|
||||
listOf("Heim", "Gast").forEach { side ->
|
||||
rows.forEach { row ->
|
||||
val isHarheimer = value(row, "${side}VereinNr") == "43030" || value(row, "${side}VereinName") == "Harheimer TC"
|
||||
val id = value(row, "${side}MannschaftId")
|
||||
if (!isHarheimer || id.isBlank()) return@forEach
|
||||
|
||||
val teamName = value(row, "${side}Mannschaft")
|
||||
val ageClass = value(row, "${side}MannschaftAltersklasse")
|
||||
val teamNumber = value(row, "${side}MannschaftNr")
|
||||
val label = listOf(teamName, ageClass, teamNumber.takeIf { it.isNotBlank() }?.let { "Nr. $it" })
|
||||
.filterNotNull()
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" · ")
|
||||
teams.putIfAbsent(id, ImportedSpielplanTeam(id, "$label [$id]"))
|
||||
}
|
||||
}
|
||||
return teams.values.sortedBy { it.label }
|
||||
}
|
||||
|
||||
private fun sportSpielplanCsvText(headers: List<String>, rows: List<List<String>>): String {
|
||||
if (headers.isEmpty()) return ""
|
||||
return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } }
|
||||
}
|
||||
|
||||
private fun parseSportCsvText(text: String): Pair<List<String>, List<List<String>>> {
|
||||
val lines = text.lineSequence().filter { it.isNotBlank() }.toList()
|
||||
if (lines.isEmpty()) return emptyList<String>() to emptyList()
|
||||
val headers = parseDelimitedLine(lines.first(), ';')
|
||||
val rows = lines.drop(1).map { parseDelimitedLine(it, ';') }
|
||||
return headers to rows
|
||||
}
|
||||
|
||||
private fun parseDelimitedLine(line: String, delimiter: Char): List<String> {
|
||||
val values = mutableListOf<String>()
|
||||
val value = StringBuilder()
|
||||
var quoted = false
|
||||
var index = 0
|
||||
while (index < line.length) {
|
||||
when (val char = line[index]) {
|
||||
'"' -> {
|
||||
if (quoted && index + 1 < line.length && line[index + 1] == '"') {
|
||||
value.append('"')
|
||||
index++
|
||||
} else {
|
||||
quoted = !quoted
|
||||
}
|
||||
}
|
||||
delimiter -> if (quoted) value.append(char) else {
|
||||
values += value.toString()
|
||||
value.clear()
|
||||
}
|
||||
else -> value.append(char)
|
||||
}
|
||||
index++
|
||||
}
|
||||
values += value.toString()
|
||||
return values
|
||||
}
|
||||
|
||||
private fun String.csvCell(delimiter: String): String {
|
||||
val needsQuotes = contains(delimiter) || contains('"') || contains('\n') || contains('\r')
|
||||
val escaped = replace("\"", "\"\"")
|
||||
return if (needsQuotes) "\"$escaped\"" else escaped
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -619,11 +936,13 @@ fun CmsNewsletterScreen(
|
||||
if (!state.loading && state.newsletters.isEmpty()) item { EmptyCard("Keine Newsletter gefunden.") }
|
||||
items(state.newsletters.size) { index ->
|
||||
val item = state.newsletters[index]
|
||||
NewsletterCard(item,
|
||||
NewsletterCard(
|
||||
item,
|
||||
canWrite = canWrite,
|
||||
onEdit = { nl ->
|
||||
editingNewsletter = nl
|
||||
nlTitle = nl.title
|
||||
nlContent = nl.title ?: ""
|
||||
nlContent = nl.title
|
||||
nlType = "subscription"
|
||||
nlTargetGroup = ""
|
||||
nlSendToExternal = true
|
||||
@@ -637,7 +956,9 @@ fun CmsNewsletterScreen(
|
||||
if (!state.loading && state.newsletterGroups.isEmpty()) item { EmptyCard("Keine Gruppen gefunden.") }
|
||||
items(state.newsletterGroups.size) { index ->
|
||||
val group = state.newsletterGroups[index]
|
||||
NewsletterGroupCard(group,
|
||||
NewsletterGroupCard(
|
||||
group,
|
||||
canWrite = canWrite,
|
||||
onEdit = { g ->
|
||||
editingGroup = g
|
||||
grpName = g.name
|
||||
@@ -753,6 +1074,12 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
var websiteVorname by remember { mutableStateOf("") }
|
||||
var websiteNachname by remember { mutableStateOf("") }
|
||||
var websiteEmail by remember { mutableStateOf("") }
|
||||
var ortName by remember { mutableStateOf("") }
|
||||
var ortStrasse by remember { mutableStateOf("") }
|
||||
var ortPlz by remember { mutableStateOf("") }
|
||||
var ortOrt by remember { mutableStateOf("") }
|
||||
val trainingTimes = remember { mutableStateListOf<de.harheimertc.data.TrainingTimeDto>() }
|
||||
val trainers = remember { mutableStateListOf<de.harheimertc.data.TrainerDto>() }
|
||||
|
||||
LaunchedEffect(config) {
|
||||
config?.let {
|
||||
@@ -764,6 +1091,14 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
websiteVorname = it.website.verantwortlicher.vorname
|
||||
websiteNachname = it.website.verantwortlicher.nachname
|
||||
websiteEmail = it.website.verantwortlicher.email
|
||||
ortName = it.training.ort.name
|
||||
ortStrasse = it.training.ort.strasse
|
||||
ortPlz = it.training.ort.plz
|
||||
ortOrt = it.training.ort.ort
|
||||
trainingTimes.clear()
|
||||
trainingTimes.addAll(it.training.zeiten)
|
||||
trainers.clear()
|
||||
trainers.addAll(it.trainer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +1125,16 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
email = websiteEmail,
|
||||
),
|
||||
),
|
||||
training = config.training.copy(
|
||||
ort = config.training.ort.copy(
|
||||
name = ortName,
|
||||
strasse = ortStrasse,
|
||||
plz = ortPlz,
|
||||
ort = ortOrt,
|
||||
),
|
||||
zeiten = trainingTimes.toList(),
|
||||
),
|
||||
trainer = trainers.toList(),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -822,6 +1167,63 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
||||
OutlinedTextField(value = websiteEmail, onValueChange = { websiteEmail = it }, label = { Text("E-Mail") }, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingsort") {
|
||||
OutlinedTextField(value = ortName, onValueChange = { ortName = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = ortStrasse, onValueChange = { ortStrasse = it }, label = { Text("Straße") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedTextField(value = ortPlz, onValueChange = { ortPlz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(value = ortOrt, onValueChange = { ortOrt = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainingszeiten") {
|
||||
trainingTimes.forEachIndexed { index, zeit ->
|
||||
TrainingTimeEditorCard(
|
||||
zeit = zeit,
|
||||
onChange = { updated -> trainingTimes[index] = updated },
|
||||
onRemove = { trainingTimes.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainingTimes.add(
|
||||
de.harheimertc.data.TrainingTimeDto(
|
||||
id = "training-${System.currentTimeMillis()}",
|
||||
tag = "Montag",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainingszeit hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Trainer") {
|
||||
trainers.forEachIndexed { index, trainer ->
|
||||
TrainerEditorCard(
|
||||
trainer = trainer,
|
||||
onChange = { updated -> trainers[index] = updated },
|
||||
onRemove = { trainers.removeAt(index) },
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
trainers.add(
|
||||
de.harheimertc.data.TrainerDto(
|
||||
id = "trainer-${System.currentTimeMillis()}",
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Trainer hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
DataCard("Systemstatus") {
|
||||
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
||||
@@ -950,6 +1352,7 @@ private fun CmsSummaryGrid(navController: NavController, state: CmsUiState) {
|
||||
val cards = listOf(
|
||||
Triple("Startseite", "Öffentliche Startseite", Destinations.CmsStartseite.route),
|
||||
Triple("Inhalte", "Vereinsseiten", Destinations.CmsInhalte.route),
|
||||
Triple("News", "Interne und öffentliche News", Destinations.CmsNews.route),
|
||||
Triple("Sportbetrieb", "Training und Sportdaten", Destinations.CmsSportbetrieb.route),
|
||||
Triple("Mitgliederverwaltung", "${state.users.size} Benutzer", Destinations.CmsMitgliederverwaltung.route),
|
||||
Triple("Kontaktanfragen", "${state.contactRequests.size} Anfragen", Destinations.CmsContactRequests.route),
|
||||
@@ -1229,29 +1632,44 @@ private fun ContactRequestCard(request: ContactRequestDto, viewModel: CmsViewMod
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsletterCard(newsletter: NewsletterDto, onEdit: (NewsletterDto) -> Unit = {}, onDelete: (String) -> Unit = {}, onSend: (String) -> Unit = {}) {
|
||||
private fun NewsletterCard(
|
||||
newsletter: NewsletterDto,
|
||||
canWrite: Boolean,
|
||||
onEdit: (NewsletterDto) -> Unit = {},
|
||||
onDelete: (String) -> Unit = {},
|
||||
onSend: (String) -> Unit = {},
|
||||
) {
|
||||
DataCard(newsletter.subject.ifBlank { newsletter.title.ifBlank { newsletter.id } }) {
|
||||
InfoRow("Status", newsletter.status ?: if (newsletter.sentAt != null) "versendet" else "Entwurf")
|
||||
InfoRow("Erstellt", newsletter.createdAt ?: "-")
|
||||
InfoRow("Versendet", newsletter.sentAt ?: "-")
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
if (newsletter.status != "sent") {
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
||||
if (canWrite) {
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
if (newsletter.status != "sent") {
|
||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsletterGroupCard(group: NewsletterGroupDto, onEdit: (NewsletterGroupDto) -> Unit = {}, onDelete: (String) -> Unit = {}) {
|
||||
private fun NewsletterGroupCard(
|
||||
group: NewsletterGroupDto,
|
||||
canWrite: Boolean,
|
||||
onEdit: (NewsletterGroupDto) -> Unit = {},
|
||||
onDelete: (String) -> Unit = {},
|
||||
) {
|
||||
DataCard(group.name.ifBlank { group.id }) {
|
||||
InfoRow("Beschreibung", group.description.ifBlank { "-" })
|
||||
InfoRow("Abonnenten", group.subscribers.size.toString())
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
if (canWrite) {
|
||||
Row {
|
||||
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
||||
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
206
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsViewModels.kt
Normal file → Executable file
206
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsViewModels.kt
Normal file → Executable file
@@ -13,6 +13,10 @@ import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||
import de.harheimertc.data.PasswordResetAttemptDto
|
||||
import de.harheimertc.data.NewsDto
|
||||
import de.harheimertc.data.NewsSaveRequest
|
||||
import de.harheimertc.data.SeasonDto
|
||||
import de.harheimertc.data.SpielDto
|
||||
import de.harheimertc.data.TerminDto
|
||||
import de.harheimertc.repositories.CmsMannschaftRow
|
||||
import de.harheimertc.repositories.CmsRepository
|
||||
import de.harheimertc.repositories.MeisterschaftResult
|
||||
import kotlinx.coroutines.async
|
||||
@@ -39,6 +43,16 @@ data class CmsUiState(
|
||||
val passwordResetFailedOnly: Boolean = true,
|
||||
val news: List<NewsDto> = emptyList(),
|
||||
val meisterschaften: List<MeisterschaftResult> = emptyList(),
|
||||
val sportLoading: Boolean = false,
|
||||
val sportSaving: Boolean = false,
|
||||
val sportTermine: List<TerminDto> = emptyList(),
|
||||
val sportMannschaften: List<CmsMannschaftRow> = emptyList(),
|
||||
val sportMannschaftenSeasons: List<String> = emptyList(),
|
||||
val sportMannschaftenSeason: String = "",
|
||||
val sportSpielplanHeaders: List<String> = emptyList(),
|
||||
val sportSpielplanRows: List<List<String>> = emptyList(),
|
||||
val sportSpielplanSeason: String = "",
|
||||
val sportSpielplanSeasons: List<SeasonDto> = emptyList(),
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -179,6 +193,183 @@ class CmsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSportbetrieb() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportLoading = true, error = null, message = null)
|
||||
|
||||
val termineRes = async { repository.managedTermine() }
|
||||
val seasonsRes = async { repository.mannschaftenSeasons() }
|
||||
val spielplanRes = async { repository.spielplan() }
|
||||
|
||||
val termineResult = termineRes.await()
|
||||
val seasonsResult = seasonsRes.await()
|
||||
val seasonInfo = seasonsResult.getOrNull()
|
||||
val selectedSeason = _state.value.sportMannschaftenSeason.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.defaultSeason?.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.currentSeason?.takeIf { it.isNotBlank() }
|
||||
?: seasonInfo?.seasons?.firstOrNull().orEmpty()
|
||||
val mannschaftenResult = repository.mannschaften(selectedSeason.takeIf { it.isNotBlank() })
|
||||
val spielplanResult = spielplanRes.await()
|
||||
val errors = listOfNotNull(
|
||||
ErrorMapper.mapError(termineResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(seasonsResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(mannschaftenResult.exceptionOrNull()),
|
||||
ErrorMapper.mapError(spielplanResult.exceptionOrNull()),
|
||||
)
|
||||
val spielplan = spielplanResult.getOrNull()
|
||||
val headers = spielplan?.headers.orEmpty()
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
sportLoading = false,
|
||||
sportTermine = termineResult.getOrNull().orEmpty(),
|
||||
sportMannschaften = mannschaftenResult.getOrNull().orEmpty(),
|
||||
sportMannschaftenSeasons = seasonInfo?.seasons.orEmpty(),
|
||||
sportMannschaftenSeason = selectedSeason,
|
||||
sportSpielplanHeaders = headers,
|
||||
sportSpielplanRows = spielplan?.data.orEmpty().map { row -> headers.map { header -> row.valueForHeader(header) } },
|
||||
sportSpielplanSeason = spielplan?.season.orEmpty(),
|
||||
sportSpielplanSeasons = spielplan?.seasons.orEmpty(),
|
||||
error = errors.takeIf { it.isNotEmpty() }?.joinToString("; "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSportMannschaftenSeason(season: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportLoading = true, error = null, message = null, sportMannschaftenSeason = season)
|
||||
repository.mannschaften(season)
|
||||
.onSuccess { rows ->
|
||||
_state.value = _state.value.copy(sportLoading = false, sportMannschaften = rows)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportLoading = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Mannschaften konnten nicht geladen werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportTermin(original: TerminDto?, termin: TerminDto) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
if (original != null) {
|
||||
repository.deleteTermin(original)
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Alter Termin konnte nicht ersetzt werden.",
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
val saveResult = repository.saveTermin(termin)
|
||||
saveResult
|
||||
.onSuccess { response ->
|
||||
val termine = repository.managedTermine().getOrDefault(_state.value.sportTermine)
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportTermine = termine,
|
||||
message = response.message ?: "Termin gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Termin konnte nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSportTermin(termin: TerminDto) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.deleteTermin(termin)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportTermine = _state.value.sportTermine.filterNot { it == termin },
|
||||
message = response.message ?: "Termin gelöscht.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Termin konnte nicht gelöscht werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportMannschaften(season: String, rows: List<CmsMannschaftRow>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.saveMannschaften(season.takeIf { it.isNotBlank() }, rows)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportMannschaften = rows,
|
||||
message = response.message ?: "Mannschaften gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Mannschaften konnten nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importSportSpielplan() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.importSpielplan()
|
||||
.onSuccess { response ->
|
||||
if (!response.success) {
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = response.message ?: "Spielplan konnte nicht importiert werden.",
|
||||
)
|
||||
return@onSuccess
|
||||
}
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
message = response.message ?: "Spielplan aktualisiert.",
|
||||
)
|
||||
loadSportbetrieb()
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Spielplan konnte nicht importiert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSportSpielplan(headers: List<String>, rows: List<List<String>>) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||
repository.saveSpielplan(headers, rows)
|
||||
.onSuccess { response ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
sportSpielplanHeaders = headers,
|
||||
sportSpielplanRows = rows,
|
||||
message = response.message ?: "Spielplan gespeichert.",
|
||||
)
|
||||
}
|
||||
.onFailure { err ->
|
||||
_state.value = _state.value.copy(
|
||||
sportSaving = false,
|
||||
error = ErrorMapper.mapError(err) ?: "Spielplan konnte nicht gespeichert werden.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveConfig(config: ConfigResponse) {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
||||
@@ -462,3 +653,18 @@ class CmsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SpielDto.valueForHeader(header: String): String = when (header) {
|
||||
"Termin" -> termin
|
||||
"HeimMannschaft" -> heimMannschaft
|
||||
"GastMannschaft" -> gastMannschaft
|
||||
"HeimMannschaftAltersklasse" -> heimAltersklasse
|
||||
"GastMannschaftAltersklasse" -> gastAltersklasse
|
||||
"Altersklasse" -> altersklasse
|
||||
"Liga" -> liga
|
||||
"Staffel" -> staffel
|
||||
"Runde" -> runde.orEmpty()
|
||||
"SpieleHeim" -> spieleHeim
|
||||
"SpieleGast" -> spieleGast
|
||||
else -> ""
|
||||
}
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryViewModel.kt
Normal file → Executable file
30
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeScreen.kt
Normal file → Executable file
30
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeScreen.kt
Normal file → Executable file
@@ -117,20 +117,22 @@ fun HomeScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
HomeCustomizationSection(
|
||||
sections = state.homepageSections,
|
||||
spielplanSeasons = state.spielplanSeasons,
|
||||
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
||||
editEnabled = editHomeSections,
|
||||
onToggleEdit = { editHomeSections = !editHomeSections },
|
||||
onMoveUp = viewModel::moveSectionUp,
|
||||
onMoveDown = viewModel::moveSectionDown,
|
||||
onEnabledChange = viewModel::setSectionEnabled,
|
||||
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
||||
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
||||
onReset = viewModel::resetSections,
|
||||
)
|
||||
if (navigationState.canAccessFullCms) {
|
||||
item {
|
||||
HomeCustomizationSection(
|
||||
sections = state.homepageSections,
|
||||
spielplanSeasons = state.spielplanSeasons,
|
||||
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
||||
editEnabled = editHomeSections,
|
||||
onToggleEdit = { editHomeSections = !editHomeSections },
|
||||
onMoveUp = viewModel::moveSectionUp,
|
||||
onMoveDown = viewModel::moveSectionDown,
|
||||
onEnabledChange = viewModel::setSectionEnabled,
|
||||
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
||||
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
||||
onReset = viewModel::resetSections,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.error) {
|
||||
item {
|
||||
|
||||
2
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeViewModel.kt
Normal file → Executable file
2
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeViewModel.kt
Normal file → Executable file
@@ -90,7 +90,7 @@ class HomeViewModel @Inject constructor(
|
||||
loading = false,
|
||||
heroImageUrl = data.heroImageUrl,
|
||||
termine = data.termine
|
||||
.filter { it.asDateTime()?.isBefore(LocalDateTime.now()) != true }
|
||||
.filter { it.asDateTime()?.toLocalDate()?.isBefore(LocalDate.now()) != true }
|
||||
.sortedBy { it.asDateTime() }
|
||||
.take(3),
|
||||
spiele = data.spiele
|
||||
|
||||
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginScreen.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginViewModel.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationScreens.kt
Normal file → Executable file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationScreens.kt
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user