Compare commits
2 Commits
dev
...
05e25aa3d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e25aa3d1 | |||
| ecae88af78 |
20
.gitea/workflows/code-analysis.yml
Executable file → Normal file
20
.gitea/workflows/code-analysis.yml
Executable file → Normal file
@@ -10,19 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
clean: true
|
clean: true
|
||||||
# Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
|
fetch-depth: 0
|
||||||
# verhindert, dass der Runner das große Repository-Pack vollständig lädt.
|
|
||||||
fetch-depth: 1
|
|
||||||
# Der Web-Workflow braucht keine Android-Artefakte. Insbesondere liegt
|
|
||||||
# dort ein großer Heap-Dump im aktuellen Commit, der auch bei einem
|
|
||||||
# flachen Checkout übertragen würde und den Runner-Checkout abbricht.
|
|
||||||
sparse-checkout: |
|
|
||||||
/*
|
|
||||||
!/android-app/
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
|
|
||||||
- name: Ensure clean workspace
|
- name: Ensure clean workspace
|
||||||
run: |
|
run: |
|
||||||
@@ -38,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@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -121,12 +112,11 @@ jobs:
|
|||||||
|
|
||||||
- name: OSV-Scanner (SCA)
|
- name: OSV-Scanner (SCA)
|
||||||
run: |
|
run: |
|
||||||
cd "$GITHUB_WORKSPACE"
|
|
||||||
curl -L -o osv-scanner https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
|
curl -L -o osv-scanner https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
|
||||||
chmod +x osv-scanner
|
chmod +x osv-scanner
|
||||||
./osv-scanner --version
|
./osv-scanner --version
|
||||||
test -f "$GITHUB_WORKSPACE/package-lock.json"
|
test -f ./package-lock.json
|
||||||
./osv-scanner scan -L "$GITHUB_WORKSPACE/package-lock.json" --config "$GITHUB_WORKSPACE/.osv-scanner.toml"
|
./osv-scanner --lockfile ./package-lock.json
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
0
.gitea/workflows/version-gate.yml
Executable file → Normal file
0
.gitea/workflows/version-gate.yml
Executable file → Normal file
6
.github/workflows/android-ci.yml
vendored
Executable file → Normal file
6
.github/workflows/android-ci.yml
vendored
Executable file → Normal file
@@ -10,14 +10,14 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
|
|||||||
4
.gitignore
vendored
Executable file → Normal file
4
.gitignore
vendored
Executable file → Normal file
@@ -94,10 +94,6 @@ dist
|
|||||||
/android-app/**/build/
|
/android-app/**/build/
|
||||||
/android-app/local.properties
|
/android-app/local.properties
|
||||||
/android-app/gradle-local.properties
|
/android-app/gradle-local.properties
|
||||||
# Android Play Store / release artifacts are generated locally or in CI.
|
|
||||||
/android-app/**/*.aab
|
|
||||||
# JVM Heap-Dumps sind lokale Diagnoseartefakte und dürfen nie ins Repository.
|
|
||||||
*.hprof
|
|
||||||
|
|
||||||
# Build output (but keep production data!)
|
# Build output (but keep production data!)
|
||||||
.output
|
.output
|
||||||
|
|||||||
0
.gitleaks.toml
Executable file → Normal file
0
.gitleaks.toml
Executable file → Normal file
0
.gitleaksignore
Executable file → Normal file
0
.gitleaksignore
Executable file → Normal file
@@ -1,4 +0,0 @@
|
|||||||
[[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
Executable file → Normal file
0
.semgrepignore
Executable file → Normal file
0
ANDROID_ARCHITECTURE.md
Executable file → Normal file
0
ANDROID_ARCHITECTURE.md
Executable file → Normal file
0
ANDROID_KOTLIN_PLAN.md
Executable file → Normal file
0
ANDROID_KOTLIN_PLAN.md
Executable file → Normal file
0
ANDROID_PORT_TODO.md
Executable file → Normal file
0
ANDROID_PORT_TODO.md
Executable file → Normal file
0
ANDROID_REPO_ENDPOINTS.md
Executable file → Normal file
0
ANDROID_REPO_ENDPOINTS.md
Executable file → Normal file
0
AUTH_README.md
Executable file → Normal file
0
AUTH_README.md
Executable file → Normal file
0
DATENSCHUTZ.md
Executable file → Normal file
0
DATENSCHUTZ.md
Executable file → Normal file
0
DATENSCHUTZ_UEBERSICHT.md
Executable file → Normal file
0
DATENSCHUTZ_UEBERSICHT.md
Executable file → Normal file
0
DEPLOYMENT.md
Executable file → Normal file
0
DEPLOYMENT.md
Executable file → Normal file
0
android-app/PLAYSTORE_ASSETS.md
Executable file → Normal file
0
android-app/PLAYSTORE_ASSETS.md
Executable file → Normal file
4
android-app/app/build.gradle.kts
Executable file → Normal file
4
android-app/app/build.gradle.kts
Executable file → Normal file
@@ -78,12 +78,12 @@ val ensureProductionApiBaseUrl = tasks.register("ensureProductionApiBaseUrl") {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "de.harheimertc"
|
namespace = "de.harheimertc"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "de.harheimertc"
|
applicationId = "de.harheimertc"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 35
|
||||||
versionCode = androidVersionCode
|
versionCode = androidVersionCode
|
||||||
versionName = androidVersionName
|
versionName = androidVersionName
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
android-app/app/instantTest/release/app-instantTest-release.aab
Normal file
BIN
android-app/app/instantTest/release/app-instantTest-release.aab
Normal file
Binary file not shown.
BIN
android-app/app/local/release/app-local-release.aab
Normal file
BIN
android-app/app/local/release/app-local-release.aab
Normal file
Binary file not shown.
0
android-app/app/proguard-rules.pro
vendored
Executable file → Normal file
0
android-app/app/proguard-rules.pro
vendored
Executable file → Normal file
0
android-app/app/src/androidTest/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/androidTest/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestBindingsModule.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestBindingsModule.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/test/TestHiltModules.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/TestActivity.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/TestActivity.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsActivateResendTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsActivateResendTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsExistingScreensSmokeTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsPasswordResetDiagnosticsScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsPasswordResetDiagnosticsScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsRolesDialogTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsRolesDialogTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsStartseiteSmokeTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsStartseiteSmokeTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsUiAutomatorClickTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/cms/CmsUiAutomatorClickTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/gallery/GalleryScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/gallery/GalleryScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/home/HomeScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/home/HomeScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/login/LoginScreenTest.kt
Executable file → Normal file
0
android-app/app/src/androidTest/java/de/harheimertc/ui/screens/login/LoginScreenTest.kt
Executable file → Normal file
0
android-app/app/src/debug/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/debug/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/main/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/main/AndroidManifest.xml
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/HarheimerApplication.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/HarheimerApplication.kt
Executable file → Normal file
28
android-app/app/src/main/java/de/harheimertc/MainActivity.kt
Executable file → Normal file
28
android-app/app/src/main/java/de/harheimertc/MainActivity.kt
Executable file → Normal file
@@ -21,20 +21,9 @@ import de.harheimertc.ui.navigation.NavigationViewModel
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import de.harheimertc.repositories.AuthRepository
|
|
||||||
import de.harheimertc.repositories.PushTokenRepository
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@Inject
|
|
||||||
lateinit var authRepository: AuthRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pushTokenRepository: PushTokenRepository
|
|
||||||
|
|
||||||
private val notificationRoute = mutableStateOf<String?>(null)
|
private val notificationRoute = mutableStateOf<String?>(null)
|
||||||
|
|
||||||
private val notificationPermissionLauncher = registerForActivityResult(
|
private val notificationPermissionLauncher = registerForActivityResult(
|
||||||
@@ -61,23 +50,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
notificationRoute.value = extractNotificationRoute(intent)
|
notificationRoute.value = extractNotificationRoute(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
syncPushTokenWhenSignedIn()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-side tokens are persistent, but re-registering the current FCM
|
|
||||||
* token is cheap and idempotent. It restores a token if server data was
|
|
||||||
* ever recovered from a backup or manually repaired.
|
|
||||||
*/
|
|
||||||
private fun syncPushTokenWhenSignedIn() {
|
|
||||||
if (authRepository.getToken().isNullOrBlank() && authRepository.getRefreshToken().isNullOrBlank()) return
|
|
||||||
lifecycleScope.launch {
|
|
||||||
pushTokenRepository.registerCurrentDevice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractNotificationRoute(intent: Intent?): String? =
|
private fun extractNotificationRoute(intent: Intent?): String? =
|
||||||
intent?.getStringExtra(EXTRA_NOTIFICATION_ROUTE)?.takeIf { it.isNotBlank() }
|
intent?.getStringExtra(EXTRA_NOTIFICATION_ROUTE)?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/data/AccessTokenAuthenticator.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/AccessTokenAuthenticator.kt
Executable file → Normal file
12
android-app/app/src/main/java/de/harheimertc/data/ApiService.kt
Executable file → Normal file
12
android-app/app/src/main/java/de/harheimertc/data/ApiService.kt
Executable file → Normal file
@@ -166,7 +166,6 @@ data class MembershipResponse(
|
|||||||
val success: Boolean = false,
|
val success: Boolean = false,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val downloadUrl: String? = null,
|
val downloadUrl: String? = null,
|
||||||
val downloadToken: String? = null,
|
|
||||||
)
|
)
|
||||||
data class LoginRequest(
|
data class LoginRequest(
|
||||||
val email: String,
|
val email: String,
|
||||||
@@ -269,7 +268,6 @@ data class NotificationSettingsDto(
|
|||||||
val birthdays: Boolean = false,
|
val birthdays: Boolean = false,
|
||||||
val newContactRequest: Boolean = false,
|
val newContactRequest: Boolean = false,
|
||||||
val newUserRegistration: Boolean = false,
|
val newUserRegistration: Boolean = false,
|
||||||
val ownTtrChanges: Boolean = false,
|
|
||||||
val selectedTeamSlugs: List<String> = emptyList(),
|
val selectedTeamSlugs: List<String> = emptyList(),
|
||||||
val selectedTeamSeason: String? = null,
|
val selectedTeamSeason: String? = null,
|
||||||
val notificationTime: String = "09:00",
|
val notificationTime: String = "09:00",
|
||||||
@@ -283,7 +281,6 @@ data class PushTokenRequest(
|
|||||||
val token: String,
|
val token: String,
|
||||||
val platform: String = "android",
|
val platform: String = "android",
|
||||||
val appVersion: String? = null,
|
val appVersion: String? = null,
|
||||||
val installationId: String? = null,
|
|
||||||
)
|
)
|
||||||
data class BirthdayDto(
|
data class BirthdayDto(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
@@ -304,7 +301,6 @@ data class QttrRowDto(
|
|||||||
val playerName: String = "",
|
val playerName: String = "",
|
||||||
val clubName: String = "",
|
val clubName: String = "",
|
||||||
val currentQttr: Int? = null,
|
val currentQttr: Int? = null,
|
||||||
val currentTtr: Int? = null,
|
|
||||||
val previousQttr: Int? = null,
|
val previousQttr: Int? = null,
|
||||||
val birthdate: String? = null,
|
val birthdate: String? = null,
|
||||||
)
|
)
|
||||||
@@ -656,18 +652,12 @@ 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>
|
||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET
|
@GET
|
||||||
suspend fun downloadMembershipPdf(
|
suspend fun downloadMembershipPdf(@Url downloadUrl: String): Response<ResponseBody>
|
||||||
@Url downloadUrl: String,
|
|
||||||
@retrofit2.http.Header("X-Membership-Download-Token") downloadToken: String? = null,
|
|
||||||
): Response<ResponseBody>
|
|
||||||
|
|
||||||
@POST("/api/auth/login")
|
@POST("/api/auth/login")
|
||||||
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
|
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/data/AuthInterceptor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/AuthInterceptor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/ConnectivityMonitor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/ConnectivityMonitor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/MediaTypes.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/MediaTypes.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/NetworkModule.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/NetworkModule.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/SecureOfflineCache.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/SecureOfflineCache.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/SessionRefresher.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/data/SessionRefresher.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/di/RepositoryModule.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/di/RepositoryModule.kt
Executable file → Normal file
2
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt
Executable file → Normal file
2
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerMessagingService.kt
Executable file → Normal file
@@ -21,7 +21,7 @@ class HarheimerMessagingService : FirebaseMessagingService() {
|
|||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
pushTokenRepository.registerCurrentDevice()
|
pushTokenRepository.registerToken(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerNotifications.kt
Executable file → Normal file
9
android-app/app/src/main/java/de/harheimertc/notifications/HarheimerNotifications.kt
Executable file → Normal file
@@ -16,16 +16,14 @@ import de.harheimertc.R
|
|||||||
import de.harheimertc.ui.navigation.Destinations
|
import de.harheimertc.ui.navigation.Destinations
|
||||||
|
|
||||||
object HarheimerNotifications {
|
object HarheimerNotifications {
|
||||||
// A new ID is intentional: Android does not allow an app update to raise
|
const val DEFAULT_CHANNEL_ID = "harheimer_tc_updates"
|
||||||
// the importance of an already-created notification channel.
|
|
||||||
const val DEFAULT_CHANNEL_ID = "harheimer_tc_updates_v2"
|
|
||||||
|
|
||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
DEFAULT_CHANNEL_ID,
|
DEFAULT_CHANNEL_ID,
|
||||||
"Harheimer TC",
|
"Harheimer TC",
|
||||||
NotificationManager.IMPORTANCE_HIGH,
|
NotificationManager.IMPORTANCE_DEFAULT,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Benachrichtigungen des Harheimer TC"
|
description = "Benachrichtigungen des Harheimer TC"
|
||||||
}
|
}
|
||||||
@@ -49,7 +47,7 @@ object HarheimerNotifications {
|
|||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setContentIntent(createContentIntent(context, notificationId, data))
|
.setContentIntent(createContentIntent(context, notificationId, data))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
@@ -81,7 +79,6 @@ object HarheimerNotifications {
|
|||||||
"news", "news_expiring" -> Destinations.MemberNews.route
|
"news", "news_expiring" -> Destinations.MemberNews.route
|
||||||
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
|
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
|
||||||
"team_matches" -> Destinations.Spielplan.route
|
"team_matches" -> Destinations.Spielplan.route
|
||||||
"qttr_list_updated", "ttr_change" -> Destinations.Qttr.route
|
|
||||||
"birthdays" -> Destinations.MemberArea.route
|
"birthdays" -> Destinations.MemberArea.route
|
||||||
"contact_request" -> Destinations.CmsContactRequests.route
|
"contact_request" -> Destinations.CmsContactRequests.route
|
||||||
"user_registration" -> Destinations.CmsBenutzer.route
|
"user_registration" -> Destinations.CmsBenutzer.route
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepositoryImpl.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/AuthRepositoryImpl.kt
Executable file → Normal file
10
android-app/app/src/main/java/de/harheimertc/repositories/CmsRepository.kt
Executable file → Normal file
10
android-app/app/src/main/java/de/harheimertc/repositories/CmsRepository.kt
Executable file → Normal file
@@ -125,7 +125,6 @@ class CmsRepository @Inject constructor(
|
|||||||
spieler = values[7],
|
spieler = values[7],
|
||||||
informationenLink = values[8],
|
informationenLink = values[8],
|
||||||
letzteAktualisierung = values[9],
|
letzteAktualisierung = values[9],
|
||||||
spielplanMannschaftId = values.getOrElse(10) { "" },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,12 +146,6 @@ class CmsRepository @Inject constructor(
|
|||||||
response.body() ?: SaveCsvResponse(success = false, message = "Leere Antwort")
|
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 {
|
suspend fun spielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
|
||||||
val response = api.spielplan(season)
|
val response = api.spielplan(season)
|
||||||
if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.")
|
if (!response.isSuccessful) error("Spielplan konnte nicht geladen werden.")
|
||||||
@@ -388,7 +381,6 @@ data class CmsMannschaftRow(
|
|||||||
val spieler: String = "",
|
val spieler: String = "",
|
||||||
val informationenLink: String = "",
|
val informationenLink: String = "",
|
||||||
val letzteAktualisierung: String = "",
|
val letzteAktualisierung: String = "",
|
||||||
val spielplanMannschaftId: String = "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
||||||
@@ -403,7 +395,6 @@ private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
|||||||
"Spieler",
|
"Spieler",
|
||||||
"Weitere Informationen Link",
|
"Weitere Informationen Link",
|
||||||
"Letzte Aktualisierung",
|
"Letzte Aktualisierung",
|
||||||
"Spielplan Mannschaft ID",
|
|
||||||
).toCsvRow()
|
).toCsvRow()
|
||||||
val rows = map { row ->
|
val rows = map { row ->
|
||||||
listOf(
|
listOf(
|
||||||
@@ -417,7 +408,6 @@ private fun List<CmsMannschaftRow>.toMannschaftenCsv(): String {
|
|||||||
row.spieler,
|
row.spieler,
|
||||||
row.informationenLink,
|
row.informationenLink,
|
||||||
row.letzteAktualisierung,
|
row.letzteAktualisierung,
|
||||||
row.spielplanMannschaftId,
|
|
||||||
).toCsvRow()
|
).toCsvRow()
|
||||||
}
|
}
|
||||||
return listOf(header).plus(rows).joinToString("\n")
|
return listOf(header).plus(rows).joinToString("\n")
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/repositories/ContactRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/ContactRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/GalleryRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/GalleryRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeLayoutPreferences.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeLayoutPreferences.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/HomeRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/LoginRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/LoginRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/MannschaftenRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/MannschaftenRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/MemberAreaRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/MemberAreaRepository.kt
Executable file → Normal file
38
android-app/app/src/main/java/de/harheimertc/repositories/MembershipRepository.kt
Executable file → Normal file
38
android-app/app/src/main/java/de/harheimertc/repositories/MembershipRepository.kt
Executable file → Normal file
@@ -9,7 +9,7 @@ import java.io.File
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
data class MembershipDocument(val message: String, val uri: String? = null)
|
data class MembershipDocument(val message: String, val uri: String)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MembershipRepository @Inject constructor(
|
class MembershipRepository @Inject constructor(
|
||||||
@@ -18,33 +18,21 @@ class MembershipRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
suspend fun submit(request: MembershipRequest): Result<MembershipDocument> = runCatching {
|
suspend fun submit(request: MembershipRequest): Result<MembershipDocument> = runCatching {
|
||||||
val response = api.generateMembershipPdf(request)
|
val response = api.generateMembershipPdf(request)
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
val serverMessage = response.errorBody()?.string()
|
|
||||||
?.let { Regex("\\\"(?:statusMessage|message)\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"").find(it)?.groupValues?.getOrNull(1) ?: it }
|
|
||||||
?.takeIf { it.isNotBlank() }
|
|
||||||
error(serverMessage ?: "Der Antrag konnte nicht übermittelt werden (HTTP ${response.code()}).")
|
|
||||||
}
|
|
||||||
val body = response.body() ?: error("Leere Antwort")
|
val body = response.body() ?: error("Leere Antwort")
|
||||||
if (!body.success) error(body.message ?: "Antrag konnte nicht erstellt werden.")
|
if (!body.success) error(body.message ?: "Antrag konnte nicht erstellt werden.")
|
||||||
val uri = body.downloadUrl?.let { downloadUrl ->
|
val downloadUrl = body.downloadUrl ?: error("PDF-Download fehlt.")
|
||||||
runCatching {
|
val documentResponse = api.downloadMembershipPdf(downloadUrl)
|
||||||
val documentResponse = api.downloadMembershipPdf(downloadUrl, body.downloadToken)
|
if (!documentResponse.isSuccessful) error("PDF konnte nicht heruntergeladen werden.")
|
||||||
if (!documentResponse.isSuccessful) error("PDF konnte nicht heruntergeladen werden.")
|
val directory = File(context.cacheDir, "membership").apply { mkdirs() }
|
||||||
val directory = File(context.cacheDir, "membership").apply { mkdirs() }
|
val file = File(directory, "beitrittserklaerung.pdf")
|
||||||
val file = File(directory, "beitrittserklaerung.pdf")
|
documentResponse.body()?.byteStream()?.use { input ->
|
||||||
documentResponse.body()?.byteStream()?.use { input ->
|
file.outputStream().use { output -> input.copyTo(output) }
|
||||||
file.outputStream().use { output -> input.copyTo(output) }
|
} ?: error("Leere PDF-Antwort")
|
||||||
} ?: error("Leere PDF-Antwort")
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", file)
|
||||||
FileProvider.getUriForFile(context, "${context.packageName}.files", file).toString()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
MembershipDocument(
|
MembershipDocument(
|
||||||
message = if (uri == null) {
|
message = body.message ?: "Beitrittsformular erfolgreich erstellt.",
|
||||||
"${body.message ?: "Mitgliedschaftsantrag erfolgreich übermittelt."} Das PDF konnte nicht auf diesem Gerät gespeichert werden."
|
uri = uri.toString(),
|
||||||
} else {
|
|
||||||
body.message ?: "Beitrittsformular erfolgreich erstellt."
|
|
||||||
},
|
|
||||||
uri = uri,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/repositories/NetworkRetry.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/NetworkRetry.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/NewsletterRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/NewsletterRepository.kt
Executable file → Normal file
6
android-app/app/src/main/java/de/harheimertc/repositories/NotificationPreferencesRepository.kt
Executable file → Normal file
6
android-app/app/src/main/java/de/harheimertc/repositories/NotificationPreferencesRepository.kt
Executable file → Normal file
@@ -19,7 +19,6 @@ data class NotificationPreferences(
|
|||||||
val birthdays: Boolean = false,
|
val birthdays: Boolean = false,
|
||||||
val newContactRequest: Boolean = false,
|
val newContactRequest: Boolean = false,
|
||||||
val newUserRegistration: Boolean = false,
|
val newUserRegistration: Boolean = false,
|
||||||
val ownTtrChanges: Boolean = false,
|
|
||||||
val selectedTeamSlugs: Set<String> = emptySet(),
|
val selectedTeamSlugs: Set<String> = emptySet(),
|
||||||
val selectedTeamSeason: String? = null,
|
val selectedTeamSeason: String? = null,
|
||||||
val notificationTime: String = DEFAULT_NOTIFICATION_TIME,
|
val notificationTime: String = DEFAULT_NOTIFICATION_TIME,
|
||||||
@@ -44,7 +43,6 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
birthdays = preferences.getBoolean(KEY_BIRTHDAYS, false),
|
birthdays = preferences.getBoolean(KEY_BIRTHDAYS, false),
|
||||||
newContactRequest = preferences.getBoolean(KEY_NEW_CONTACT_REQUEST, false),
|
newContactRequest = preferences.getBoolean(KEY_NEW_CONTACT_REQUEST, false),
|
||||||
newUserRegistration = preferences.getBoolean(KEY_NEW_USER_REGISTRATION, false),
|
newUserRegistration = preferences.getBoolean(KEY_NEW_USER_REGISTRATION, false),
|
||||||
ownTtrChanges = preferences.getBoolean(KEY_OWN_TTR_CHANGES, false),
|
|
||||||
selectedTeamSlugs = preferences.getStringSet(KEY_SELECTED_TEAM_SLUGS, emptySet()).orEmpty(),
|
selectedTeamSlugs = preferences.getStringSet(KEY_SELECTED_TEAM_SLUGS, emptySet()).orEmpty(),
|
||||||
selectedTeamSeason = preferences.getString(KEY_SELECTED_TEAM_SEASON, null)?.takeIf { it.isNotBlank() },
|
selectedTeamSeason = preferences.getString(KEY_SELECTED_TEAM_SEASON, null)?.takeIf { it.isNotBlank() },
|
||||||
notificationTime = preferences.getString(KEY_NOTIFICATION_TIME, DEFAULT_NOTIFICATION_TIME) ?: DEFAULT_NOTIFICATION_TIME,
|
notificationTime = preferences.getString(KEY_NOTIFICATION_TIME, DEFAULT_NOTIFICATION_TIME) ?: DEFAULT_NOTIFICATION_TIME,
|
||||||
@@ -71,7 +69,6 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
.putBoolean(KEY_BIRTHDAYS, settings.birthdays)
|
.putBoolean(KEY_BIRTHDAYS, settings.birthdays)
|
||||||
.putBoolean(KEY_NEW_CONTACT_REQUEST, settings.newContactRequest)
|
.putBoolean(KEY_NEW_CONTACT_REQUEST, settings.newContactRequest)
|
||||||
.putBoolean(KEY_NEW_USER_REGISTRATION, settings.newUserRegistration)
|
.putBoolean(KEY_NEW_USER_REGISTRATION, settings.newUserRegistration)
|
||||||
.putBoolean(KEY_OWN_TTR_CHANGES, settings.ownTtrChanges)
|
|
||||||
.putStringSet(KEY_SELECTED_TEAM_SLUGS, settings.selectedTeamSlugs)
|
.putStringSet(KEY_SELECTED_TEAM_SLUGS, settings.selectedTeamSlugs)
|
||||||
.putString(KEY_SELECTED_TEAM_SEASON, settings.selectedTeamSeason)
|
.putString(KEY_SELECTED_TEAM_SEASON, settings.selectedTeamSeason)
|
||||||
.putString(KEY_NOTIFICATION_TIME, settings.notificationTime)
|
.putString(KEY_NOTIFICATION_TIME, settings.notificationTime)
|
||||||
@@ -101,7 +98,6 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
const val KEY_BIRTHDAYS = "birthdays"
|
const val KEY_BIRTHDAYS = "birthdays"
|
||||||
const val KEY_NEW_CONTACT_REQUEST = "new_contact_request"
|
const val KEY_NEW_CONTACT_REQUEST = "new_contact_request"
|
||||||
const val KEY_NEW_USER_REGISTRATION = "new_user_registration"
|
const val KEY_NEW_USER_REGISTRATION = "new_user_registration"
|
||||||
const val KEY_OWN_TTR_CHANGES = "own_ttr_changes"
|
|
||||||
const val KEY_SELECTED_TEAM_SLUGS = "selected_team_slugs"
|
const val KEY_SELECTED_TEAM_SLUGS = "selected_team_slugs"
|
||||||
const val KEY_SELECTED_TEAM_SEASON = "selected_team_season"
|
const val KEY_SELECTED_TEAM_SEASON = "selected_team_season"
|
||||||
const val KEY_NOTIFICATION_TIME = "notification_time"
|
const val KEY_NOTIFICATION_TIME = "notification_time"
|
||||||
@@ -118,7 +114,6 @@ private fun NotificationSettingsDto.toPreferences(): NotificationPreferences = N
|
|||||||
birthdays = birthdays,
|
birthdays = birthdays,
|
||||||
newContactRequest = newContactRequest,
|
newContactRequest = newContactRequest,
|
||||||
newUserRegistration = newUserRegistration,
|
newUserRegistration = newUserRegistration,
|
||||||
ownTtrChanges = ownTtrChanges,
|
|
||||||
selectedTeamSlugs = selectedTeamSlugs.toSet(),
|
selectedTeamSlugs = selectedTeamSlugs.toSet(),
|
||||||
selectedTeamSeason = selectedTeamSeason,
|
selectedTeamSeason = selectedTeamSeason,
|
||||||
notificationTime = notificationTime,
|
notificationTime = notificationTime,
|
||||||
@@ -134,7 +129,6 @@ private fun NotificationPreferences.toDto(): NotificationSettingsDto = Notificat
|
|||||||
birthdays = birthdays,
|
birthdays = birthdays,
|
||||||
newContactRequest = newContactRequest,
|
newContactRequest = newContactRequest,
|
||||||
newUserRegistration = newUserRegistration,
|
newUserRegistration = newUserRegistration,
|
||||||
ownTtrChanges = ownTtrChanges,
|
|
||||||
selectedTeamSlugs = selectedTeamSlugs.toList(),
|
selectedTeamSlugs = selectedTeamSlugs.toList(),
|
||||||
selectedTeamSeason = selectedTeamSeason,
|
selectedTeamSeason = selectedTeamSeason,
|
||||||
notificationTime = notificationTime,
|
notificationTime = notificationTime,
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/repositories/PasskeyRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/PasskeyRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/ProfileRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/ProfileRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/PublicPagesRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/PublicPagesRepository.kt
Executable file → Normal file
12
android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt
Executable file → Normal file
12
android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt
Executable file → Normal file
@@ -1,7 +1,6 @@
|
|||||||
package de.harheimertc.repositories
|
package de.harheimertc.repositories
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.firebase.installations.FirebaseInstallations
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import de.harheimertc.BuildConfig
|
import de.harheimertc.BuildConfig
|
||||||
import de.harheimertc.data.ApiService
|
import de.harheimertc.data.ApiService
|
||||||
@@ -16,24 +15,19 @@ class PushTokenRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
suspend fun registerCurrentDevice(): Result<Unit> = runCatching {
|
suspend fun registerCurrentDevice(): Result<Unit> = runCatching {
|
||||||
val token = FirebaseMessaging.getInstance().token.await()
|
val token = FirebaseMessaging.getInstance().token.await()
|
||||||
val installationId = FirebaseInstallations.getInstance().id.await()
|
registerToken(token).getOrThrow()
|
||||||
registerToken(token, installationId).getOrThrow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun registerToken(token: String, installationId: String? = null): Result<Unit> = runCatching {
|
suspend fun registerToken(token: String): Result<Unit> = runCatching {
|
||||||
if (token.isBlank()) return@runCatching
|
if (token.isBlank()) return@runCatching
|
||||||
retryOnNetworkFailure {
|
retryOnNetworkFailure {
|
||||||
val response = api.registerPushToken(
|
val response = api.registerPushToken(
|
||||||
PushTokenRequest(
|
PushTokenRequest(
|
||||||
token = token,
|
token = token,
|
||||||
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
|
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
|
||||||
installationId = installationId,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) error("Push-Token konnte nicht registriert werden.")
|
||||||
val detail = response.errorBody()?.string().orEmpty().replace(Regex("\\s+"), " ").take(180)
|
|
||||||
error("Push-Token konnte nicht registriert werden (HTTP ${response.code()})${if (detail.isBlank()) "" else ": $detail"}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/repositories/SpielplanRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/SpielplanRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/TermineRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/TermineRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/TrainingRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/repositories/TrainingRepository.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/security/DeviceKeyManager.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/security/DeviceKeyManager.kt
Executable file → Normal file
2
android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt
Executable file → Normal file
2
android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt
Executable file → Normal file
@@ -472,7 +472,7 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
|||||||
MenuSection.INTERN -> buildList {
|
MenuSection.INTERN -> buildList {
|
||||||
add(MenuTarget("Übersicht", Destinations.MemberArea.route))
|
add(MenuTarget("Übersicht", Destinations.MemberArea.route))
|
||||||
add(MenuTarget("Mitgliederliste", Destinations.Members.route))
|
add(MenuTarget("Mitgliederliste", Destinations.Members.route))
|
||||||
add(MenuTarget("TTR / QTTR", Destinations.Qttr.route))
|
add(MenuTarget("QTTR", Destinations.Qttr.route))
|
||||||
add(MenuTarget("News", Destinations.MemberNews.route))
|
add(MenuTarget("News", Destinations.MemberNews.route))
|
||||||
add(MenuTarget("Mein Profil", Destinations.Profile.route))
|
add(MenuTarget("Mein Profil", Destinations.Profile.route))
|
||||||
add(MenuTarget("Benachrichtigungen", Destinations.NotificationSettings.route))
|
add(MenuTarget("Benachrichtigungen", Destinations.NotificationSettings.route))
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/ui/components/FormComponents.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/FormComponents.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/HeroComponent.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/HeroComponent.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/ImageGrid.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/ImageGrid.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/LoadingState.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/LoadingState.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/NativeRichTextEditor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/NativeRichTextEditor.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/PendingPage.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/PendingPage.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichText.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichText.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichTextUtils.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/components/RichTextUtils.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/navigation/Destinations.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/navigation/Destinations.kt
Executable file → Normal file
122
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavGraph.kt
Executable file → Normal file
122
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavGraph.kt
Executable file → Normal file
@@ -2,9 +2,7 @@ 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
|
||||||
@@ -12,7 +10,6 @@ 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
|
||||||
@@ -330,141 +327,46 @@ fun NavGraph(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsStartseite.route) {
|
composable(Destinations.CmsStartseite.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsStartseiteScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsStartseiteScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsInhalte.route) {
|
composable(Destinations.CmsInhalte.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsInhalteScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsInhalteScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsVereinsmeisterschaften.route) {
|
composable(Destinations.CmsVereinsmeisterschaften.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsVereinsmeisterschaftenScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsVereinsmeisterschaftenScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsNews.route) {
|
composable(Destinations.CmsNews.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsNewsScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsNewsScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsSportbetrieb.route) {
|
composable(Destinations.CmsSportbetrieb.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsSportbetriebScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsSportbetriebScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsMitgliederverwaltung.route) {
|
composable(Destinations.CmsMitgliederverwaltung.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsMitgliederverwaltungScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsMitgliederverwaltungScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsNewsletter.route) {
|
composable(Destinations.CmsNewsletter.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsNewsletterScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessNewsletter,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsNewsletterScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsContactRequests.route) {
|
composable(Destinations.CmsContactRequests.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsContactRequestsScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessContactRequests,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsContactRequestsScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsEinstellungen.route) {
|
composable(Destinations.CmsEinstellungen.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsEinstellungenScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsEinstellungenScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsBenutzer.route) {
|
composable(Destinations.CmsBenutzer.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsBenutzerScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsBenutzerScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.CmsPasswordResetDiagnostics.route) {
|
composable(Destinations.CmsPasswordResetDiagnostics.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsPasswordResetDiagnosticsScreen(navController, !persistentNavigation)
|
||||||
allowed = navigationState.canAccessFullCms,
|
|
||||||
navController = navController,
|
|
||||||
) {
|
|
||||||
de.harheimertc.ui.screens.cms.CmsPasswordResetDiagnosticsScreen(navController, !persistentNavigation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(Destinations.Cms.route) {
|
composable(Destinations.Cms.route) {
|
||||||
RequireCmsAccess(
|
de.harheimertc.ui.screens.cms.CmsDashboardScreen(navController, !persistentNavigation)
|
||||||
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
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/navigation/NavigationViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsBenutzerScreens.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsBenutzerScreens.kt
Executable file → Normal file
34
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsNewsScreens.kt
Executable file → Normal file
34
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsNewsScreens.kt
Executable file → Normal file
@@ -132,22 +132,15 @@ 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(
|
NewsListItem(news = news, selected = selected, onSelect = { id, sel ->
|
||||||
news = news,
|
id?.let {
|
||||||
canWrite = canWrite,
|
selection = if (sel) selection + it else selection - it
|
||||||
selected = selected,
|
}
|
||||||
onSelect = { id, sel ->
|
}, onEdit = { openEdit(news) }, onDelete = { news.id?.let { id -> deletingIds = listOf(id) } })
|
||||||
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 (canWrite && selection.isNotEmpty()) {
|
if (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") }
|
||||||
@@ -262,7 +255,6 @@ 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,
|
||||||
@@ -272,11 +264,7 @@ 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(
|
Checkbox(checked = selected, onCheckedChange = { onSelect(news.id, it) })
|
||||||
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)) {
|
||||||
@@ -309,11 +297,9 @@ private fun NewsListItem(
|
|||||||
Text("Aktualisiert: ${news.updated}")
|
Text("Aktualisiert: ${news.updated}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canWrite) {
|
Row {
|
||||||
Row {
|
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
||||||
TextButton(onClick = { onEdit(news) }) { Text("Bearbeiten") }
|
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
||||||
TextButton(onClick = { news.id?.let { onDelete(it) } }) { Text("Löschen") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsScreens.kt
Executable file → Normal file
107
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsScreens.kt
Executable file → Normal file
@@ -458,9 +458,6 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
|||||||
"mannschaften" to "Mannschaften",
|
"mannschaften" to "Mannschaften",
|
||||||
"spielplaene" to "Spielpläne",
|
"spielplaene" to "Spielpläne",
|
||||||
)
|
)
|
||||||
val spielplanTeamOptions = remember(state.sportSpielplanHeaders, state.sportSpielplanRows) {
|
|
||||||
importedSpielplanTeams(state.sportSpielplanHeaders, state.sportSpielplanRows)
|
|
||||||
}
|
|
||||||
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
|
val terminKategorien = listOf("Training", "Punktspiel", "Turnier", "Veranstaltung", "Sonstiges")
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -609,7 +606,6 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
|||||||
items(mannschaften.size) { index ->
|
items(mannschaften.size) { index ->
|
||||||
MannschaftEditorCard(
|
MannschaftEditorCard(
|
||||||
row = mannschaften[index],
|
row = mannschaften[index],
|
||||||
spielplanTeams = spielplanTeamOptions,
|
|
||||||
onChange = { updated -> mannschaften[index] = updated },
|
onChange = { updated -> mannschaften[index] = updated },
|
||||||
onRemove = { mannschaften.removeAt(index) },
|
onRemove = { mannschaften.removeAt(index) },
|
||||||
)
|
)
|
||||||
@@ -623,19 +619,9 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
|||||||
InfoRow("Datei", fileName)
|
InfoRow("Datei", fileName)
|
||||||
InfoRow("Saison", seasonLabel)
|
InfoRow("Saison", seasonLabel)
|
||||||
InfoRow("Einträge", state.sportSpielplanRows.size.toString())
|
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()) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.loadSportbetrieb() },
|
onClick = { viewModel.loadSportbetrieb() },
|
||||||
enabled = !state.sportSaving,
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text("Neu laden")
|
Text("Neu laden")
|
||||||
@@ -746,32 +732,11 @@ fun CmsSportbetriebScreen(navController: NavController, showBackNavigation: Bool
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MannschaftEditorCard(
|
private fun MannschaftEditorCard(
|
||||||
row: CmsMannschaftRow,
|
row: CmsMannschaftRow,
|
||||||
spielplanTeams: List<ImportedSpielplanTeam>,
|
|
||||||
onChange: (CmsMannschaftRow) -> Unit,
|
onChange: (CmsMannschaftRow) -> Unit,
|
||||||
onRemove: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var teamPickerOpen by remember(row.mannschaft, spielplanTeams) { mutableStateOf(false) }
|
|
||||||
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
|
DataCard(row.mannschaft.ifBlank { "Mannschaft" }) {
|
||||||
OutlinedTextField(value = row.mannschaft, onValueChange = { onChange(row.copy(mannschaft = it)) }, label = { Text("Mannschaft") }, modifier = Modifier.fillMaxWidth())
|
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.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.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.telefon, onValueChange = { onChange(row.copy(telefon = it)) }, label = { Text("Telefon") }, modifier = Modifier.fillMaxWidth())
|
||||||
@@ -787,35 +752,6 @@ private fun MannschaftEditorCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private fun sportSpielplanCsvText(headers: List<String>, rows: List<List<String>>): String {
|
||||||
if (headers.isEmpty()) return ""
|
if (headers.isEmpty()) return ""
|
||||||
return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } }
|
return listOf(headers).plus(rows).joinToString("\n") { row -> row.joinToString(";") { it.csvCell(";") } }
|
||||||
@@ -936,9 +872,7 @@ 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(
|
NewsletterCard(item,
|
||||||
item,
|
|
||||||
canWrite = canWrite,
|
|
||||||
onEdit = { nl ->
|
onEdit = { nl ->
|
||||||
editingNewsletter = nl
|
editingNewsletter = nl
|
||||||
nlTitle = nl.title
|
nlTitle = nl.title
|
||||||
@@ -956,9 +890,7 @@ 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(
|
NewsletterGroupCard(group,
|
||||||
group,
|
|
||||||
canWrite = canWrite,
|
|
||||||
onEdit = { g ->
|
onEdit = { g ->
|
||||||
editingGroup = g
|
editingGroup = g
|
||||||
grpName = g.name
|
grpName = g.name
|
||||||
@@ -1632,44 +1564,29 @@ private fun ContactRequestCard(request: ContactRequestDto, viewModel: CmsViewMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewsletterCard(
|
private fun NewsletterCard(newsletter: NewsletterDto, onEdit: (NewsletterDto) -> Unit = {}, onDelete: (String) -> Unit = {}, onSend: (String) -> Unit = {}) {
|
||||||
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 ?: "-")
|
||||||
if (canWrite) {
|
Row {
|
||||||
Row {
|
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
||||||
TextButton(onClick = { onEdit(newsletter) }) { Text("Bearbeiten") }
|
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
if (newsletter.status != "sent") {
|
||||||
if (newsletter.status != "sent") {
|
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
||||||
TextButton(onClick = { newsletter.id.takeIf { it.isNotBlank() }?.let { onSend(it) } }) { Text("Versenden") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewsletterGroupCard(
|
private fun NewsletterGroupCard(group: NewsletterGroupDto, onEdit: (NewsletterGroupDto) -> Unit = {}, onDelete: (String) -> Unit = {}) {
|
||||||
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())
|
||||||
if (canWrite) {
|
Row {
|
||||||
Row {
|
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
||||||
TextButton(onClick = { onEdit(group) }) { Text("Bearbeiten") }
|
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
||||||
TextButton(onClick = { group.id.takeIf { it.isNotBlank() }?.let { onDelete(it) } }) { Text("Löschen") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsViewModels.kt
Executable file → Normal file
27
android-app/app/src/main/java/de/harheimertc/ui/screens/cms/CmsViewModels.kt
Executable file → Normal file
@@ -322,33 +322,6 @@ class CmsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>>) {
|
fun saveSportSpielplan(headers: List<String>, rows: List<List<String>>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
_state.value = _state.value.copy(sportSaving = true, error = null, message = null)
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactScreen.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactScreen.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/contact/ContactViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryScreen.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryScreen.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/gallery/GalleryViewModel.kt
Executable file → Normal file
30
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeScreen.kt
Executable file → Normal file
30
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeScreen.kt
Executable file → Normal file
@@ -117,22 +117,20 @@ fun HomeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (navigationState.canAccessFullCms) {
|
item {
|
||||||
item {
|
HomeCustomizationSection(
|
||||||
HomeCustomizationSection(
|
sections = state.homepageSections,
|
||||||
sections = state.homepageSections,
|
spielplanSeasons = state.spielplanSeasons,
|
||||||
spielplanSeasons = state.spielplanSeasons,
|
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
||||||
spielplanTeamsBySeason = state.spielplanTeamsBySeason,
|
editEnabled = editHomeSections,
|
||||||
editEnabled = editHomeSections,
|
onToggleEdit = { editHomeSections = !editHomeSections },
|
||||||
onToggleEdit = { editHomeSections = !editHomeSections },
|
onMoveUp = viewModel::moveSectionUp,
|
||||||
onMoveUp = viewModel::moveSectionUp,
|
onMoveDown = viewModel::moveSectionDown,
|
||||||
onMoveDown = viewModel::moveSectionDown,
|
onEnabledChange = viewModel::setSectionEnabled,
|
||||||
onEnabledChange = viewModel::setSectionEnabled,
|
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
||||||
onAddSpielplanWidget = viewModel::addSpielplanTeamWidget,
|
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
||||||
onUpdateSpielplanWidget = viewModel::updateSpielplanTeamWidget,
|
onReset = viewModel::resetSections,
|
||||||
onReset = viewModel::resetSections,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (state.error) {
|
if (state.error) {
|
||||||
item {
|
item {
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/home/HomeViewModel.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginScreen.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginScreen.kt
Executable file → Normal file
4
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginViewModel.kt
Executable file → Normal file
4
android-app/app/src/main/java/de/harheimertc/ui/screens/login/LoginViewModel.kt
Executable file → Normal file
@@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import de.harheimertc.repositories.LoginRepository
|
import de.harheimertc.repositories.LoginRepository
|
||||||
import de.harheimertc.repositories.PasskeyRepository
|
import de.harheimertc.repositories.PasskeyRepository
|
||||||
import de.harheimertc.repositories.PushTokenRepository
|
|
||||||
import de.harheimertc.ui.components.isValidEmail
|
import de.harheimertc.ui.components.isValidEmail
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -30,7 +29,6 @@ data class LoginUiState(
|
|||||||
class LoginViewModel @Inject constructor(
|
class LoginViewModel @Inject constructor(
|
||||||
private val repository: LoginRepository,
|
private val repository: LoginRepository,
|
||||||
private val passkeyRepository: PasskeyRepository,
|
private val passkeyRepository: PasskeyRepository,
|
||||||
private val pushTokenRepository: PushTokenRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(LoginUiState())
|
private val _state = MutableStateFlow(LoginUiState())
|
||||||
val state: StateFlow<LoginUiState> = _state
|
val state: StateFlow<LoginUiState> = _state
|
||||||
@@ -72,7 +70,6 @@ class LoginViewModel @Inject constructor(
|
|||||||
_state.value = current.copy(loading = true, error = null, message = null)
|
_state.value = current.copy(loading = true, error = null, message = null)
|
||||||
repository.login(current.email, current.password)
|
repository.login(current.email, current.password)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
viewModelScope.launch { pushTokenRepository.registerCurrentDevice() }
|
|
||||||
_state.value = current.copy(
|
_state.value = current.copy(
|
||||||
password = "",
|
password = "",
|
||||||
loading = false,
|
loading = false,
|
||||||
@@ -95,7 +92,6 @@ class LoginViewModel @Inject constructor(
|
|||||||
_state.value = current.copy(loading = true, error = null, message = null)
|
_state.value = current.copy(loading = true, error = null, message = null)
|
||||||
passkeyRepository.login(context, current.email)
|
passkeyRepository.login(context, current.email)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
viewModelScope.launch { pushTokenRepository.registerCurrentDevice() }
|
|
||||||
_state.value = current.copy(
|
_state.value = current.copy(
|
||||||
password = "",
|
password = "",
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|||||||
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationScreens.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationScreens.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationViewModels.kt
Executable file → Normal file
0
android-app/app/src/main/java/de/harheimertc/ui/screens/login/RegistrationViewModels.kt
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user