Compare commits
25 Commits
c7a306e8fa
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
281c25b05d | ||
|
|
1beb5c2eee | ||
|
|
461f2de8dc | ||
|
|
75a7c409cd | ||
|
|
751c302036 | ||
|
|
f0e1ad39b0 | ||
|
|
1dc84023d0 | ||
|
|
30465c7833 | ||
|
|
d260f00756 | ||
|
|
f0ac4b7e56 | ||
|
|
bd45beadd5 | ||
|
|
f060c9771f | ||
|
|
0ab71166aa | ||
|
|
d9c5d242ec | ||
|
|
cb1e7a2fed | ||
|
|
3586704cce | ||
|
|
7f3b58c5bc | ||
|
|
f29e8a4dac | ||
|
|
b69130c2b2 | ||
|
|
77aabef4a9 | ||
|
|
4b699de853 | ||
|
|
e3cb7282bc | ||
|
|
e537839e28 | ||
|
|
44d441811c | ||
|
|
da1efa5a74 |
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
clean: true
|
clean: true
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
echo "eslint entries in package.json:" && rg '"eslint"' package.json || true
|
echo "eslint entries in package.json:" && rg '"eslint"' package.json || true
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
chmod +x osv-scanner
|
chmod +x osv-scanner
|
||||||
./osv-scanner --version
|
./osv-scanner --version
|
||||||
test -f ./package-lock.json
|
test -f ./package-lock.json
|
||||||
./osv-scanner --lockfile ./package-lock.json
|
./osv-scanner scan -L ./package-lock.json --config ./.osv-scanner.toml
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
runs-on: ubuntu-latest
|
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:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.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
|
versionName = androidVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable += setOf(
|
||||||
|
"AutoboxingStateCreation",
|
||||||
|
"MutableCollectionMutableState",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
if (hasReleaseSigning) {
|
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 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 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 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(
|
data class TerminDto(
|
||||||
val datum: String = "",
|
val datum: String = "",
|
||||||
val uhrzeit: String? = null,
|
val uhrzeit: String? = null,
|
||||||
@@ -231,7 +232,7 @@ data class ProfileVisibilityDto(
|
|||||||
val showEmail: Boolean = true,
|
val showEmail: Boolean = true,
|
||||||
val showPhone: Boolean = true,
|
val showPhone: Boolean = true,
|
||||||
val showAddress: Boolean = false,
|
val showAddress: Boolean = false,
|
||||||
val showBirthday: Boolean = true,
|
val showBirthday: Boolean = false,
|
||||||
)
|
)
|
||||||
data class ProfileUserDto(
|
data class ProfileUserDto(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
@@ -328,6 +329,7 @@ data class MemberDto(
|
|||||||
val editable: Boolean = false,
|
val editable: Boolean = false,
|
||||||
val isMannschaftsspieler: Boolean = false,
|
val isMannschaftsspieler: Boolean = false,
|
||||||
val hasHallKey: Boolean = false,
|
val hasHallKey: Boolean = false,
|
||||||
|
val showBirthday: Boolean = false,
|
||||||
val loginRoles: List<String> = emptyList(),
|
val loginRoles: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
data class MembersResponse(
|
data class MembersResponse(
|
||||||
@@ -590,6 +592,21 @@ interface ApiService {
|
|||||||
@GET("/api/termine")
|
@GET("/api/termine")
|
||||||
suspend fun termine(): Response<TermineResponse>
|
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")
|
@GET("/api/spielplan")
|
||||||
suspend fun spielplan(@Query("season") season: String? = null): Response<SpielplanResponse>
|
suspend fun spielplan(@Query("season") season: String? = null): Response<SpielplanResponse>
|
||||||
|
|
||||||
@@ -635,6 +652,9 @@ interface ApiService {
|
|||||||
@POST("/api/cms/save-csv")
|
@POST("/api/cms/save-csv")
|
||||||
suspend fun saveCsv(@Body request: SaveCsvRequest): Response<SaveCsvResponse>
|
suspend fun saveCsv(@Body request: SaveCsvRequest): Response<SaveCsvResponse>
|
||||||
|
|
||||||
|
@POST("/api/cms/import-spielplan")
|
||||||
|
suspend fun importSpielplan(): Response<AuthMessageResponse>
|
||||||
|
|
||||||
@POST("/api/membership/generate-pdf")
|
@POST("/api/membership/generate-pdf")
|
||||||
suspend fun generateMembershipPdf(@Body request: MembershipRequest): Response<MembershipResponse>
|
suspend fun generateMembershipPdf(@Body request: MembershipRequest): Response<MembershipResponse>
|
||||||
|
|
||||||
@@ -713,6 +733,7 @@ interface ApiService {
|
|||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
val isMannschaftsspieler: Boolean = false,
|
val isMannschaftsspieler: Boolean = false,
|
||||||
val hasHallKey: Boolean = false,
|
val hasHallKey: Boolean = false,
|
||||||
|
val showBirthday: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BulkImportRequest(val members: List<Map<String, String>>)
|
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
|
@Singleton
|
||||||
class ConnectivityMonitor @Inject constructor(
|
class ConnectivityMonitor @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private val _online = MutableStateFlow(hasInternetAccess())
|
private val _online = MutableStateFlow(hasInternetAccess())
|
||||||
@@ -46,4 +46,4 @@ class ConnectivityMonitor @Inject constructor(
|
|||||||
val capabilities = manager.getNetworkCapabilities(network) ?: return false
|
val capabilities = manager.getNetworkCapabilities(network) ?: return false
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
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))
|
.setContentIntent(createContentIntent(context, notificationId, data))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
return try {
|
||||||
return true
|
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
||||||
|
true
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createContentIntent(context: Context, notificationId: Int, payload: Map<String, String>): PendingIntent {
|
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"]) {
|
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
|
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.SaveCsvRequest
|
||||||
import de.harheimertc.data.SaveCsvResponse
|
import de.harheimertc.data.SaveCsvResponse
|
||||||
import de.harheimertc.data.SecureOfflineCache
|
import de.harheimertc.data.SecureOfflineCache
|
||||||
|
import de.harheimertc.data.SpielplanResponse
|
||||||
|
import de.harheimertc.data.TerminDto
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CmsRepository @Inject constructor(
|
class CmsRepository @Inject constructor(
|
||||||
@@ -83,6 +85,93 @@ class CmsRepository @Inject constructor(
|
|||||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
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> =
|
suspend fun users(): Result<CmsUsersResponse> =
|
||||||
fetchEncryptedFallback(
|
fetchEncryptedFallback(
|
||||||
load = {
|
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>> =
|
private fun parseCsv(csv: String): List<List<String>> =
|
||||||
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
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.CmsStartseite.route,
|
||||||
Destinations.CmsInhalte.route,
|
Destinations.CmsInhalte.route,
|
||||||
Destinations.CmsVereinsmeisterschaften.route,
|
Destinations.CmsVereinsmeisterschaften.route,
|
||||||
|
Destinations.CmsNews.route,
|
||||||
Destinations.CmsSportbetrieb.route,
|
Destinations.CmsSportbetrieb.route,
|
||||||
Destinations.CmsMitgliederverwaltung.route,
|
Destinations.CmsMitgliederverwaltung.route,
|
||||||
Destinations.CmsNewsletter.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("Startseite", Destinations.CmsStartseite.route))
|
||||||
add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
||||||
add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.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("Sportbetrieb", Destinations.CmsSportbetrieb.route))
|
||||||
add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
|
add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
|
||||||
add(MenuTarget("Einstellungen", Destinations.CmsEinstellungen.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 CmsStartseite : Destinations("cms/startseite")
|
||||||
object CmsInhalte : Destinations("cms/inhalte")
|
object CmsInhalte : Destinations("cms/inhalte")
|
||||||
object CmsVereinsmeisterschaften : Destinations("cms/vereinsmeisterschaften")
|
object CmsVereinsmeisterschaften : Destinations("cms/vereinsmeisterschaften")
|
||||||
|
object CmsNews : Destinations("cms/news")
|
||||||
object CmsSportbetrieb : Destinations("cms/sportbetrieb")
|
object CmsSportbetrieb : Destinations("cms/sportbetrieb")
|
||||||
object CmsMitgliederverwaltung : Destinations("cms/mitgliederverwaltung")
|
object CmsMitgliederverwaltung : Destinations("cms/mitgliederverwaltung")
|
||||||
object CmsNewsletter : Destinations("cms/newsletter")
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -327,43 +330,141 @@ fun NavGraph(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsStartseite.route) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
private fun NavHostController.navigateTopLevel(route: String) {
|
||||||
val isTeamDetail = route.startsWith("mannschaften/") &&
|
val isTeamDetail = route.startsWith("mannschaften/") &&
|
||||||
route != Destinations.Spielsysteme.route
|
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 ->
|
items(state.news) { news ->
|
||||||
val selected = news.id?.let { selection.contains(it) } ?: false
|
val selected = news.id?.let { selection.contains(it) } ?: false
|
||||||
NewsListItem(news = news, selected = selected, onSelect = { id, sel ->
|
NewsListItem(
|
||||||
id?.let {
|
news = news,
|
||||||
selection = if (sel) selection + it else selection - it
|
canWrite = canWrite,
|
||||||
}
|
selected = selected,
|
||||||
}, onEdit = { openEdit(news) }, onDelete = { news.id?.let { id -> deletingIds = listOf(id) } })
|
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
|
// bulk action bar
|
||||||
if (selection.isNotEmpty()) {
|
if (canWrite && selection.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Button(onClick = { viewModel.bulkSetPublic(selection.toList(), true) }) { Text("Als öffentlich markieren") }
|
Button(onClick = { viewModel.bulkSetPublic(selection.toList(), true) }) { Text("Als öffentlich markieren") }
|
||||||
@@ -255,6 +262,7 @@ fun CmsNewsScreen(navController: NavController, showBackNavigation: Boolean, vie
|
|||||||
@Composable
|
@Composable
|
||||||
private fun NewsListItem(
|
private fun NewsListItem(
|
||||||
news: NewsDto,
|
news: NewsDto,
|
||||||
|
canWrite: Boolean,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
onSelect: (String?, Boolean) -> Unit = { _, _ -> },
|
onSelect: (String?, Boolean) -> Unit = { _, _ -> },
|
||||||
onEdit: (NewsDto) -> Unit,
|
onEdit: (NewsDto) -> Unit,
|
||||||
@@ -264,7 +272,11 @@ private fun NewsListItem(
|
|||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
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))
|
Text(news.title.ifBlank { "(Ohne Titel)" }, modifier = Modifier.padding(start = 8.dp))
|
||||||
if (news.isPublic) {
|
if (news.isPublic) {
|
||||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp)) {
|
||||||
@@ -297,9 +309,11 @@ private fun NewsListItem(
|
|||||||
Text("Aktualisiert: ${news.updated}")
|
Text("Aktualisiert: ${news.updated}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row {
|
if (canWrite) {
|
||||||
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
Row {
|
||||||
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
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
|
package de.harheimertc.ui.screens.cms
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -36,6 +38,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -48,10 +52,12 @@ import de.harheimertc.data.NewsletterGroupDto
|
|||||||
import de.harheimertc.data.PasswordResetMatchingUserDto
|
import de.harheimertc.data.PasswordResetMatchingUserDto
|
||||||
import de.harheimertc.data.PasswordResetAttemptDto
|
import de.harheimertc.data.PasswordResetAttemptDto
|
||||||
import de.harheimertc.data.PasswordResetStepDto
|
import de.harheimertc.data.PasswordResetStepDto
|
||||||
|
import de.harheimertc.data.TerminDto
|
||||||
import de.harheimertc.ui.components.FormMessages
|
import de.harheimertc.ui.components.FormMessages
|
||||||
import de.harheimertc.ui.components.LoadingState
|
import de.harheimertc.ui.components.LoadingState
|
||||||
import de.harheimertc.ui.components.NativeRichTextEditor
|
import de.harheimertc.ui.components.NativeRichTextEditor
|
||||||
import de.harheimertc.ui.navigation.Destinations
|
import de.harheimertc.ui.navigation.Destinations
|
||||||
|
import de.harheimertc.repositories.CmsMannschaftRow
|
||||||
import de.harheimertc.repositories.MeisterschaftResult
|
import de.harheimertc.repositories.MeisterschaftResult
|
||||||
import de.harheimertc.ui.theme.Accent100
|
import de.harheimertc.ui.theme.Accent100
|
||||||
import de.harheimertc.ui.theme.Accent500
|
import de.harheimertc.ui.theme.Accent500
|
||||||
@@ -61,6 +67,7 @@ import de.harheimertc.ui.theme.Primary100
|
|||||||
import de.harheimertc.ui.theme.Primary600
|
import de.harheimertc.ui.theme.Primary600
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -433,116 +440,426 @@ fun CmsVereinsmeisterschaftenScreen(navController: NavController, showBackNaviga
|
|||||||
@Composable
|
@Composable
|
||||||
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Boolean, viewModel: CmsViewModel = hiltViewModel()) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val config = state.config
|
val context = LocalContext.current
|
||||||
var ortName by remember { mutableStateOf("") }
|
var activeTab by remember { mutableStateOf("termine") }
|
||||||
var ortStrasse by remember { mutableStateOf("") }
|
val mannschaften = remember { mutableStateListOf<CmsMannschaftRow>() }
|
||||||
var ortPlz by remember { mutableStateOf("") }
|
var spielplanCsv by remember { mutableStateOf("") }
|
||||||
var ortOrt by remember { mutableStateOf("") }
|
var spielplanEditorOpen by remember { mutableStateOf(false) }
|
||||||
val trainingTimes = remember { mutableStateListOf<de.harheimertc.data.TrainingTimeDto>() }
|
var terminDialogOpen by remember { mutableStateOf(false) }
|
||||||
val trainers = remember { mutableStateListOf<de.harheimertc.data.TrainerDto>() }
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
config?.let {
|
viewModel.loadSportbetrieb()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CmsPage(navController, showBackNavigation, "Sportbetrieb", "Trainingszeiten, Trainingsort und Trainer pflegen") {
|
LaunchedEffect(state.sportMannschaften) {
|
||||||
when {
|
mannschaften.clear()
|
||||||
state.loading || config == null -> item { LoadingState("Sportbetriebsdaten werden geladen...") }
|
mannschaften.addAll(state.sportMannschaften)
|
||||||
else -> {
|
}
|
||||||
item {
|
|
||||||
Button(
|
LaunchedEffect(state.sportSpielplanHeaders, state.sportSpielplanRows) {
|
||||||
onClick = {
|
spielplanCsv = sportSpielplanCsvText(state.sportSpielplanHeaders, state.sportSpielplanRows)
|
||||||
viewModel.saveConfig(
|
}
|
||||||
config.copy(
|
|
||||||
training = config.training.copy(
|
fun openTerminDialog(termin: TerminDto?) {
|
||||||
ort = config.training.ort.copy(
|
editingTermin = termin
|
||||||
name = ortName,
|
terminDatum = termin?.datum.orEmpty()
|
||||||
strasse = ortStrasse,
|
terminUhrzeit = termin?.uhrzeit.orEmpty()
|
||||||
plz = ortPlz,
|
terminTitel = termin?.titel.orEmpty()
|
||||||
ort = ortOrt,
|
terminBeschreibung = termin?.beschreibung.orEmpty()
|
||||||
),
|
terminKategorie = termin?.kategorie ?: "Sonstiges"
|
||||||
zeiten = trainingTimes.toList(),
|
terminDialogOpen = true
|
||||||
),
|
}
|
||||||
trainer = trainers.toList(),
|
|
||||||
),
|
fun openDatePicker() {
|
||||||
)
|
val calendar = Calendar.getInstance()
|
||||||
},
|
runCatching {
|
||||||
enabled = !state.saving,
|
val parts = terminDatum.split("-")
|
||||||
modifier = Modifier.fillMaxWidth(),
|
if (parts.size == 3) {
|
||||||
) {
|
calendar.set(parts[0].toInt(), parts[1].toInt() - 1, parts[2].toInt())
|
||||||
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) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
@Composable
|
||||||
@@ -619,11 +936,13 @@ fun CmsNewsletterScreen(
|
|||||||
if (!state.loading && state.newsletters.isEmpty()) item { EmptyCard("Keine Newsletter gefunden.") }
|
if (!state.loading && state.newsletters.isEmpty()) item { EmptyCard("Keine Newsletter gefunden.") }
|
||||||
items(state.newsletters.size) { index ->
|
items(state.newsletters.size) { index ->
|
||||||
val item = state.newsletters[index]
|
val item = state.newsletters[index]
|
||||||
NewsletterCard(item,
|
NewsletterCard(
|
||||||
|
item,
|
||||||
|
canWrite = canWrite,
|
||||||
onEdit = { nl ->
|
onEdit = { nl ->
|
||||||
editingNewsletter = nl
|
editingNewsletter = nl
|
||||||
nlTitle = nl.title
|
nlTitle = nl.title
|
||||||
nlContent = nl.title ?: ""
|
nlContent = nl.title
|
||||||
nlType = "subscription"
|
nlType = "subscription"
|
||||||
nlTargetGroup = ""
|
nlTargetGroup = ""
|
||||||
nlSendToExternal = true
|
nlSendToExternal = true
|
||||||
@@ -637,7 +956,9 @@ fun CmsNewsletterScreen(
|
|||||||
if (!state.loading && state.newsletterGroups.isEmpty()) item { EmptyCard("Keine Gruppen gefunden.") }
|
if (!state.loading && state.newsletterGroups.isEmpty()) item { EmptyCard("Keine Gruppen gefunden.") }
|
||||||
items(state.newsletterGroups.size) { index ->
|
items(state.newsletterGroups.size) { index ->
|
||||||
val group = state.newsletterGroups[index]
|
val group = state.newsletterGroups[index]
|
||||||
NewsletterGroupCard(group,
|
NewsletterGroupCard(
|
||||||
|
group,
|
||||||
|
canWrite = canWrite,
|
||||||
onEdit = { g ->
|
onEdit = { g ->
|
||||||
editingGroup = g
|
editingGroup = g
|
||||||
grpName = g.name
|
grpName = g.name
|
||||||
@@ -753,6 +1074,12 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
|||||||
var websiteVorname by remember { mutableStateOf("") }
|
var websiteVorname by remember { mutableStateOf("") }
|
||||||
var websiteNachname by remember { mutableStateOf("") }
|
var websiteNachname by remember { mutableStateOf("") }
|
||||||
var websiteEmail 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) {
|
LaunchedEffect(config) {
|
||||||
config?.let {
|
config?.let {
|
||||||
@@ -764,6 +1091,14 @@ fun CmsEinstellungenScreen(navController: NavController, showBackNavigation: Boo
|
|||||||
websiteVorname = it.website.verantwortlicher.vorname
|
websiteVorname = it.website.verantwortlicher.vorname
|
||||||
websiteNachname = it.website.verantwortlicher.nachname
|
websiteNachname = it.website.verantwortlicher.nachname
|
||||||
websiteEmail = it.website.verantwortlicher.email
|
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,
|
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())
|
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 {
|
item {
|
||||||
DataCard("Systemstatus") {
|
DataCard("Systemstatus") {
|
||||||
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
InfoRow("Mitgliedschaftstarife", config.mitgliedschaft.size.toString())
|
||||||
@@ -950,6 +1352,7 @@ private fun CmsSummaryGrid(navController: NavController, state: CmsUiState) {
|
|||||||
val cards = listOf(
|
val cards = listOf(
|
||||||
Triple("Startseite", "Öffentliche Startseite", Destinations.CmsStartseite.route),
|
Triple("Startseite", "Öffentliche Startseite", Destinations.CmsStartseite.route),
|
||||||
Triple("Inhalte", "Vereinsseiten", Destinations.CmsInhalte.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("Sportbetrieb", "Training und Sportdaten", Destinations.CmsSportbetrieb.route),
|
||||||
Triple("Mitgliederverwaltung", "${state.users.size} Benutzer", Destinations.CmsMitgliederverwaltung.route),
|
Triple("Mitgliederverwaltung", "${state.users.size} Benutzer", Destinations.CmsMitgliederverwaltung.route),
|
||||||
Triple("Kontaktanfragen", "${state.contactRequests.size} Anfragen", Destinations.CmsContactRequests.route),
|
Triple("Kontaktanfragen", "${state.contactRequests.size} Anfragen", Destinations.CmsContactRequests.route),
|
||||||
@@ -1229,29 +1632,44 @@ private fun ContactRequestCard(request: ContactRequestDto, viewModel: CmsViewMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 } }) {
|
DataCard(newsletter.subject.ifBlank { newsletter.title.ifBlank { newsletter.id } }) {
|
||||||
InfoRow("Status", newsletter.status ?: if (newsletter.sentAt != null) "versendet" else "Entwurf")
|
InfoRow("Status", newsletter.status ?: if (newsletter.sentAt != null) "versendet" else "Entwurf")
|
||||||
InfoRow("Erstellt", newsletter.createdAt ?: "-")
|
InfoRow("Erstellt", newsletter.createdAt ?: "-")
|
||||||
InfoRow("Versendet", newsletter.sentAt ?: "-")
|
InfoRow("Versendet", newsletter.sentAt ?: "-")
|
||||||
Row {
|
if (canWrite) {
|
||||||
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
Row {
|
||||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
||||||
if (newsletter.status != "sent") {
|
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
if (newsletter.status != "sent") {
|
||||||
|
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 }) {
|
DataCard(group.name.ifBlank { group.id }) {
|
||||||
InfoRow("Beschreibung", group.description.ifBlank { "-" })
|
InfoRow("Beschreibung", group.description.ifBlank { "-" })
|
||||||
InfoRow("Abonnenten", group.subscribers.size.toString())
|
InfoRow("Abonnenten", group.subscribers.size.toString())
|
||||||
Row {
|
if (canWrite) {
|
||||||
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
Row {
|
||||||
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
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.PasswordResetAttemptDto
|
||||||
import de.harheimertc.data.NewsDto
|
import de.harheimertc.data.NewsDto
|
||||||
import de.harheimertc.data.NewsSaveRequest
|
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.CmsRepository
|
||||||
import de.harheimertc.repositories.MeisterschaftResult
|
import de.harheimertc.repositories.MeisterschaftResult
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -39,6 +43,16 @@ data class CmsUiState(
|
|||||||
val passwordResetFailedOnly: Boolean = true,
|
val passwordResetFailedOnly: Boolean = true,
|
||||||
val news: List<NewsDto> = emptyList(),
|
val news: List<NewsDto> = emptyList(),
|
||||||
val meisterschaften: List<MeisterschaftResult> = 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
|
@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) {
|
fun saveConfig(config: ConfigResponse) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.value = _state.value.copy(saving = true, error = null, message = null)
|
_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 {
|
if (navigationState.canAccessFullCms) {
|
||||||
HomeCustomizationSection(
|
item {
|
||||||
sections = state.homepageSections,
|
HomeCustomizationSection(
|
||||||
spielplanSeasons = state.spielplanSeasons,
|
sections = state.homepageSections,
|
||||||
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
spielplanSeasons = state.spielplanSeasons,
|
||||||
editEnabled = editHomeSections,
|
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
||||||
onToggleEdit = { editHomeSections = !editHomeSections },
|
editEnabled = editHomeSections,
|
||||||
onMoveUp = viewModel::moveSectionUp,
|
onToggleEdit = { editHomeSections = !editHomeSections },
|
||||||
onMoveDown = viewModel::moveSectionDown,
|
onMoveUp = viewModel::moveSectionUp,
|
||||||
onEnabledChange = viewModel::setSectionEnabled,
|
onMoveDown = viewModel::moveSectionDown,
|
||||||
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
onEnabledChange = viewModel::setSectionEnabled,
|
||||||
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
||||||
onReset = viewModel::resetSections,
|
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
||||||
)
|
onReset = viewModel::resetSections,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.error) {
|
if (state.error) {
|
||||||
item {
|
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,
|
loading = false,
|
||||||
heroImageUrl = data.heroImageUrl,
|
heroImageUrl = data.heroImageUrl,
|
||||||
termine = data.termine
|
termine = data.termine
|
||||||
.filter { it.asDateTime()?.isBefore(LocalDateTime.now()) != true }
|
.filter { it.asDateTime()?.toLocalDate()?.isBefore(LocalDate.now()) != true }
|
||||||
.sortedBy { it.asDateTime() }
|
.sortedBy { it.asDateTime() }
|
||||||
.take(3),
|
.take(3),
|
||||||
spiele = data.spiele
|
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