feat(Tournament): add official tournament participation feature
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
- Introduced functionality to load and display official tournament participation events in the CalendarView. - Updated the API client to fetch official tournament data, enhancing the event management capabilities. - Added new UI elements to represent official tournaments, including visual indicators and event details. - Enhanced the ClubManager to support fetching and managing official tournament data. - Updated the mobile app's TODO list to reflect progress on tournament-related features.
This commit is contained in:
@@ -1008,7 +1008,7 @@ class TournamentService {
|
||||
return JSON.parse(JSON.stringify(tournaments));
|
||||
}
|
||||
|
||||
Ve // 2. Neues Turnier anlegen
|
||||
// 2. Neues Turnier anlegen
|
||||
async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal, isDoublesTournament = false) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await Tournament.create({
|
||||
|
||||
@@ -138,6 +138,7 @@ export default {
|
||||
activeTypes: {
|
||||
training: true,
|
||||
tournament: true,
|
||||
officialTournament: true,
|
||||
match: true,
|
||||
holiday: true,
|
||||
schoolHoliday: true,
|
||||
@@ -160,6 +161,7 @@ export default {
|
||||
return [
|
||||
{ key: 'training', label: 'Training' },
|
||||
{ key: 'tournament', label: 'Turnier' },
|
||||
{ key: 'officialTournament', label: 'Teilnahme' },
|
||||
{ key: 'match', label: 'Punktspiel' },
|
||||
{ key: 'holiday', label: 'Feiertag' },
|
||||
{ key: 'schoolHoliday', label: 'Ferien' },
|
||||
@@ -188,7 +190,7 @@ export default {
|
||||
return this.events.reduce((counts, event) => {
|
||||
counts[event.type] = (counts[event.type] || 0) + 1;
|
||||
return counts;
|
||||
}, { training: 0, tournament: 0, match: 0, holiday: 0, schoolHoliday: 0, trainingCancellation: 0 });
|
||||
}, { training: 0, tournament: 0, officialTournament: 0, match: 0, holiday: 0, schoolHoliday: 0, trainingCancellation: 0 });
|
||||
},
|
||||
visibleTrainingCancellations() {
|
||||
const month = this.cursor.getMonth();
|
||||
@@ -253,6 +255,7 @@ export default {
|
||||
this.loadSource('Trainingszeiten', () => this.loadRecurringTrainingEvents()),
|
||||
this.loadSource('Trainingsausfälle', () => this.loadTrainingCancellationEvents()),
|
||||
this.loadSource('Turniere', () => this.loadTournamentEvents()),
|
||||
this.loadSource('Turnierteilnahmen', () => this.loadOfficialTournamentEvents()),
|
||||
this.loadSource('Punktspiele', () => this.loadMatchEvents()),
|
||||
this.loadSource('Ferien/Feiertage', () => this.loadHolidayEvents())
|
||||
]);
|
||||
@@ -435,6 +438,31 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
async loadOfficialTournamentEvents() {
|
||||
const response = await apiClient.get(`/official-tournaments/${this.currentClub}/participations/summary`);
|
||||
this.ensureSuccess(response, 'Turnierteilnahmen');
|
||||
return (response.data || [])
|
||||
.filter(tournament => Array.isArray(tournament.entries) && tournament.entries.length > 0)
|
||||
.map(tournament => {
|
||||
const date = this.parseDmyDate(tournament.startDate);
|
||||
const endDate = this.parseDmyDate(tournament.endDate || tournament.startDate);
|
||||
if (!date) return null;
|
||||
const entries = tournament.entries || [];
|
||||
const participantCount = new Set(entries.map(entry => entry.memberId).filter(Boolean)).size;
|
||||
return {
|
||||
id: `official-tournament-${tournament.tournamentId}`,
|
||||
type: 'officialTournament',
|
||||
date,
|
||||
endDate: endDate || date,
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: tournament.tournamentName || tournament.title || 'Turnierteilnahme',
|
||||
subtitle: participantCount > 0 ? `${participantCount} Teilnehmer` : `${entries.length} Starts`,
|
||||
route: '/tournament-participations'
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
async loadHolidayEvents() {
|
||||
const response = await apiClient.get(`/calendar/club/${this.currentClub}/holidays`, {
|
||||
params: { year: this.displayedYear }
|
||||
@@ -483,6 +511,12 @@ export default {
|
||||
if (!year || !month || !day) return new Date(value);
|
||||
return new Date(year, month - 1, day);
|
||||
},
|
||||
parseDmyDate(value) {
|
||||
const match = String(value || '').match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
|
||||
if (!match) return null;
|
||||
const date = new Date(Number(match[3]), Number(match[2]) - 1, Number(match[1]));
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
},
|
||||
combineDateTime(date, time) {
|
||||
if (!date) return Number.POSITIVE_INFINITY;
|
||||
const result = new Date(date);
|
||||
@@ -659,6 +693,7 @@ export default {
|
||||
|
||||
.legend-training::before { background: #2f7a5f; }
|
||||
.legend-tournament::before { background: #b7791f; }
|
||||
.legend-officialTournament::before { background: #9333ea; }
|
||||
.legend-match::before { background: #2563eb; }
|
||||
.legend-holiday::before { background: #dc2626; }
|
||||
.legend-schoolHoliday::before { background: #7c3aed; }
|
||||
@@ -823,6 +858,7 @@ export default {
|
||||
|
||||
.event-training { background: #e8f4ef; border-left: 4px solid #2f7a5f; }
|
||||
.event-tournament { background: #fff7e6; border-left: 4px solid #b7791f; }
|
||||
.event-officialTournament { background: #f3e8ff; border-left: 4px solid #9333ea; }
|
||||
.event-match { background: #eaf1ff; border-left: 4px solid #2563eb; }
|
||||
.event-holiday { background: #fee2e2; border-left: 4px solid #dc2626; }
|
||||
.event-schoolHoliday { background: #f3e8ff; border-left: 4px solid #7c3aed; }
|
||||
|
||||
@@ -154,10 +154,10 @@ Web: `DiaryView.vue` (sehr groß). API-Cluster (Auszug – in Web nach `apiClien
|
||||
|
||||
## Phase 7 – Turniere
|
||||
|
||||
- [ ] `TournamentsView.vue` – Vereinsturniere
|
||||
- [ ] `OfficialTournaments.vue` / offizielle Teilnahmen
|
||||
- [ ] `TournamentTab.vue` – eingebettete Logik, soweit mobil relevant
|
||||
- [ ] API aus jeweiligen Views dokumentieren und abarbeiten
|
||||
- [x] **Vereins-Turniere** (`TournamentsView.vue` / `TournamentTab.vue`): `GET /api/tournament/:clubId` (optional `?type=mini`), clientseitig intern/offen wie Web; `GET /api/tournament/:clubId/:id` für Detail – UI `TournamentsScreen.kt`, `TournamentsApi`, `ClubInternalTournamentsManager`
|
||||
- [x] **Offizielle Teilnahmen** (`OfficialTournaments.vue`): `GET /api/official-tournaments/:clubId`, `GET .../participations/summary` – `OfficialTournamentsApi`, `OfficialTournamentsReadManager`, gleicher Screen
|
||||
- [x] **Navigation:** Tab **Turniere** (`MainTab.Tournaments`) bei `canReadTournaments()`, Start-Kachel; Deep-Link `/tournaments` für volle Web-Verwaltung
|
||||
- [ ] **Nicht mobil:** PDF-Import, Meldungen/ClickTT-Workflows, Spielstände pflegen, Gruppen/K.O. wie Web – bewusst Web-only
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import de.tt_tagebuch.shared.api.PredefinedActivitiesApi
|
||||
import de.tt_tagebuch.shared.api.MatchesApi
|
||||
import de.tt_tagebuch.shared.api.MemberActivitiesApi
|
||||
import de.tt_tagebuch.shared.api.MemberGroupPhotosApi
|
||||
import de.tt_tagebuch.shared.api.MemberTransferConfigApi
|
||||
import de.tt_tagebuch.shared.api.MembersApi
|
||||
import de.tt_tagebuch.shared.api.OfficialTournamentsApi
|
||||
import de.tt_tagebuch.shared.api.PermissionsApi
|
||||
@@ -45,7 +46,6 @@ import de.tt_tagebuch.shared.state.MutableTokenProvider
|
||||
import de.tt_tagebuch.shared.state.OfficialTournamentsReadManager
|
||||
import de.tt_tagebuch.shared.state.PendingApprovalsManager
|
||||
import de.tt_tagebuch.shared.state.PermissionsAdminManager
|
||||
import de.tt_tagebuch.shared.state.MutableTokenProvider
|
||||
import de.tt_tagebuch.shared.state.ScheduleManager
|
||||
import de.tt_tagebuch.shared.state.TrainingStatsManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -87,6 +87,7 @@ class AppDependencies(context: Context) {
|
||||
)
|
||||
|
||||
private val permissionsApi = PermissionsApi(client)
|
||||
val predefinedActivitiesApi = PredefinedActivitiesApi(client)
|
||||
|
||||
val clubManager = ClubManager(
|
||||
clubStorage = AndroidClubStorage(context.applicationContext),
|
||||
@@ -99,6 +100,7 @@ class AppDependencies(context: Context) {
|
||||
val apiLogsManager = ApiLogsManager(ApiLogsApi(client))
|
||||
val clubInternalTournamentsManager = ClubInternalTournamentsManager(TournamentsApi(client))
|
||||
val officialTournamentsReadManager = OfficialTournamentsReadManager(OfficialTournamentsApi(client))
|
||||
val memberTransferConfigApi = MemberTransferConfigApi(client)
|
||||
|
||||
val diaryManager = DiaryManager(
|
||||
DiaryApi(client),
|
||||
@@ -106,7 +108,7 @@ class AppDependencies(context: Context) {
|
||||
GroupApi(client),
|
||||
DiaryMemberActivitiesApi(client),
|
||||
DiaryMemberApi(client),
|
||||
PredefinedActivitiesApi(client),
|
||||
predefinedActivitiesApi,
|
||||
AccidentApi(client),
|
||||
MemberGroupPhotosApi(client),
|
||||
)
|
||||
|
||||
@@ -211,8 +211,9 @@ private fun MainTabs(dependencies: AppDependencies) {
|
||||
val clubState by dependencies.clubManager.state.collectAsState()
|
||||
val visibleTabs = visibleMainTabs(clubState.currentPermissions)
|
||||
|
||||
LaunchedEffect(visibleTabs, selectedTab) {
|
||||
if (!visibleTabs.contains(selectedTab)) {
|
||||
LaunchedEffect(clubState.currentPermissions, selectedTab) {
|
||||
val tabs = visibleMainTabs(clubState.currentPermissions)
|
||||
if (!tabs.contains(selectedTab)) {
|
||||
selectedTab = MainTab.Home
|
||||
}
|
||||
}
|
||||
@@ -380,6 +381,13 @@ private fun HomeScreen(
|
||||
onClick = { onOpenTab(MainTab.Schedule) },
|
||||
)
|
||||
}
|
||||
if (p.canReadTournaments()) {
|
||||
HomeHubTile(
|
||||
title = tr("navigation.clubTournaments", "Turniere"),
|
||||
subtitle = tr("home.tileTournaments", "Vereins-Turniere, Teilnahmen"),
|
||||
onClick = { onOpenTab(MainTab.Tournaments) },
|
||||
)
|
||||
}
|
||||
}
|
||||
HomeHubTile(
|
||||
title = tr("navigation.statistics", "Statistik"),
|
||||
@@ -4079,6 +4087,8 @@ private fun SettingsScreen(dependencies: AppDependencies) {
|
||||
dependencies.pendingApprovalsManager.clear()
|
||||
dependencies.permissionsAdminManager.clear()
|
||||
dependencies.apiLogsManager.clear()
|
||||
dependencies.clubInternalTournamentsManager.clear()
|
||||
dependencies.officialTournamentsReadManager.clear()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
package de.tt_tagebuch.app.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.tt_tagebuch.app.AppDependencies
|
||||
import de.tt_tagebuch.shared.api.models.Club
|
||||
import de.tt_tagebuch.shared.api.models.MemberDataQualityRequirements
|
||||
import de.tt_tagebuch.shared.api.models.MemberTransferConfigEnvelope
|
||||
import de.tt_tagebuch.shared.api.models.PredefinedActivityDto
|
||||
import de.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
|
||||
import de.tt_tagebuch.shared.api.models.UpdateClubSettingsBody
|
||||
import de.tt_tagebuch.shared.api.models.canReadClubSettings
|
||||
import de.tt_tagebuch.shared.api.models.canReadMembers
|
||||
import de.tt_tagebuch.shared.api.models.canReadPredefinedActivities
|
||||
import de.tt_tagebuch.shared.api.models.canWriteClubSettings
|
||||
import de.tt_tagebuch.shared.api.models.canWriteMembers
|
||||
import de.tt_tagebuch.shared.api.models.canWritePredefinedActivities
|
||||
import de.tt_tagebuch.shared.api.models.displayLabel
|
||||
import de.tt_tagebuch.shared.i18n.MobileStrings
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
|
||||
private val StammdatenPad = 20.dp
|
||||
private val StammdatenTouchMin = 48.dp
|
||||
|
||||
private data class GermanStateOption(val code: String, val name: String)
|
||||
|
||||
private val germanStates: List<GermanStateOption> = listOf(
|
||||
GermanStateOption("DE-BW", "Baden-Württemberg"),
|
||||
GermanStateOption("DE-BY", "Bayern"),
|
||||
GermanStateOption("DE-BE", "Berlin"),
|
||||
GermanStateOption("DE-BB", "Brandenburg"),
|
||||
GermanStateOption("DE-HB", "Bremen"),
|
||||
GermanStateOption("DE-HH", "Hamburg"),
|
||||
GermanStateOption("DE-HE", "Hessen"),
|
||||
GermanStateOption("DE-MV", "Mecklenburg-Vorpommern"),
|
||||
GermanStateOption("DE-NI", "Niedersachsen"),
|
||||
GermanStateOption("DE-NW", "Nordrhein-Westfalen"),
|
||||
GermanStateOption("DE-RP", "Rheinland-Pfalz"),
|
||||
GermanStateOption("DE-SL", "Saarland"),
|
||||
GermanStateOption("DE-SN", "Sachsen"),
|
||||
GermanStateOption("DE-ST", "Sachsen-Anhalt"),
|
||||
GermanStateOption("DE-SH", "Schleswig-Holstein"),
|
||||
GermanStateOption("DE-TH", "Thüringen"),
|
||||
)
|
||||
|
||||
internal enum class ClubStammdatenSection {
|
||||
ClubSettings,
|
||||
PredefinedActivities,
|
||||
MemberTransfer,
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ClubStammdatenFlowScreen(
|
||||
destination: ClubStammdatenSection,
|
||||
dependencies: AppDependencies,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = onBack)
|
||||
when (destination) {
|
||||
ClubStammdatenSection.ClubSettings -> MobileClubSettingsScreen(dependencies, onBack)
|
||||
ClubStammdatenSection.PredefinedActivities -> MobilePredefinedActivitiesScreen(dependencies, onBack)
|
||||
ClubStammdatenSection.MemberTransfer -> MobileMemberTransferScreen(dependencies, onBack)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StammdatenTopBar(title: String, onBack: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
Text(title, style = MaterialTheme.typography.h6, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
private fun defaultQuality(): MemberDataQualityRequirements = MemberDataQualityRequirements()
|
||||
|
||||
private fun normalizeQuality(m: MemberDataQualityRequirements?): MemberDataQualityRequirements {
|
||||
val d = defaultQuality()
|
||||
if (m == null) return d
|
||||
return MemberDataQualityRequirements(
|
||||
requireStreet = m.requireStreet,
|
||||
requirePostalCode = m.requirePostalCode,
|
||||
requireCity = m.requireCity,
|
||||
requirePhone = m.requirePhone,
|
||||
requireEmail = m.requireEmail,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MobileClubSettingsScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
val clubState by dependencies.clubManager.state.collectAsState()
|
||||
val clubId = clubState.currentClubId ?: return
|
||||
val perms = clubState.currentPermissions
|
||||
val scope = rememberCoroutineScope()
|
||||
var club by remember { mutableStateOf<Club?>(null) }
|
||||
var loadError by remember { mutableStateOf<String?>(null) }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var saving by remember { mutableStateOf(false) }
|
||||
var savedHint by remember { mutableStateOf(false) }
|
||||
|
||||
var greeting by remember { mutableStateOf("") }
|
||||
var associationNumber by remember { mutableStateOf("") }
|
||||
var countryCode by remember { mutableStateOf("DE") }
|
||||
var stateCode by remember { mutableStateOf("") }
|
||||
var stateMenu by remember { mutableStateOf(false) }
|
||||
var myTtNickname by remember { mutableStateOf("") }
|
||||
var autoFetch by remember { mutableStateOf(false) }
|
||||
var quality by remember { mutableStateOf(defaultQuality()) }
|
||||
|
||||
LaunchedEffect(clubId) {
|
||||
loading = true
|
||||
loadError = null
|
||||
club = runCatching { dependencies.clubManager.fetchClubDetail(clubId) }
|
||||
.onFailure { loadError = it.message }
|
||||
.getOrNull()
|
||||
val c = club
|
||||
if (c != null) {
|
||||
greeting = c.greetingText.orEmpty()
|
||||
associationNumber = c.associationMemberNumber.orEmpty()
|
||||
countryCode = c.countryCode?.ifBlank { "DE" } ?: "DE"
|
||||
stateCode = c.stateCode.orEmpty()
|
||||
myTtNickname = c.myTischtennisFedNickname.orEmpty()
|
||||
autoFetch = c.autoFetchRankings == true
|
||||
quality = normalizeQuality(c.memberDataQualityRequirements)
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(StammdatenPad),
|
||||
) {
|
||||
StammdatenTopBar(tr("clubSettings.title", "Vereinseinstellungen"), onBack)
|
||||
if (perms?.canReadClubSettings() != true) {
|
||||
Text(tr("mobile.noAccess", "Keine Berechtigung."))
|
||||
return@Column
|
||||
}
|
||||
when {
|
||||
loading -> CircularProgressIndicator(modifier = Modifier.padding(top = 24.dp))
|
||||
loadError != null -> Text(loadError!!, color = MaterialTheme.colors.error)
|
||||
club == null -> Text(tr("clubSettings.loadFailed", "Laden fehlgeschlagen"))
|
||||
else -> {
|
||||
Text(tr("clubSettings.greetingText", "Begrüßung"), fontWeight = FontWeight.SemiBold)
|
||||
OutlinedTextField(
|
||||
value = greeting,
|
||||
onValueChange = { greeting = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
minLines = 4,
|
||||
label = { Text(tr("clubSettings.greetingPlaceholder", "Text")) },
|
||||
)
|
||||
Text(
|
||||
tr("clubSettings.greetingHint", "Platzhalter: {home}, {guest}, …"),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(tr("clubSettings.associationMemberNumber", "Verbands-Nr."), fontWeight = FontWeight.SemiBold)
|
||||
OutlinedTextField(
|
||||
value = associationNumber,
|
||||
onValueChange = { associationNumber = it },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Kalenderregion", fontWeight = FontWeight.SemiBold)
|
||||
Text("Land", style = MaterialTheme.typography.caption)
|
||||
OutlinedTextField(
|
||||
value = countryCode,
|
||||
onValueChange = { countryCode = it.uppercase().take(2) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
singleLine = true,
|
||||
enabled = false,
|
||||
)
|
||||
Text("Bundesland", style = MaterialTheme.typography.caption, modifier = Modifier.padding(top = 8.dp))
|
||||
Box {
|
||||
TextButton(onClick = { stateMenu = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
germanStates.find { it.code == stateCode }?.name
|
||||
?: tr("mobile.stateNotSet", "Nicht gesetzt"),
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = stateMenu, onDismissRequest = { stateMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
stateCode = ""
|
||||
stateMenu = false
|
||||
},
|
||||
) { Text(tr("mobile.stateNotSet", "Nicht gesetzt")) }
|
||||
germanStates.forEach { s ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
stateCode = s.code
|
||||
stateMenu = false
|
||||
},
|
||||
) { Text(s.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(tr("clubSettings.myTischtennisRankings", "MyTischtennis-Rankings"), fontWeight = FontWeight.SemiBold)
|
||||
RowSwitch(tr("clubSettings.autoFetchRankings", "Automatisch abrufen"), autoFetch) { autoFetch = it }
|
||||
if (autoFetch) {
|
||||
OutlinedTextField(
|
||||
value = myTtNickname,
|
||||
onValueChange = { myTtNickname = it },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
label = { Text(tr("clubSettings.myTischtennisFedNickname", "Verbands-Kurzname")) },
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(tr("clubSettings.memberDataQuality", "Pflichtfelder Mitglieder"), fontWeight = FontWeight.SemiBold)
|
||||
RowSwitch(tr("clubSettings.requireStreet", "Straße"), quality.requireStreet) {
|
||||
quality = quality.copy(requireStreet = it)
|
||||
}
|
||||
RowSwitch(tr("clubSettings.requirePostalCode", "PLZ"), quality.requirePostalCode) {
|
||||
quality = quality.copy(requirePostalCode = it)
|
||||
}
|
||||
RowSwitch(tr("clubSettings.requireCity", "Ort"), quality.requireCity) {
|
||||
quality = quality.copy(requireCity = it)
|
||||
}
|
||||
RowSwitch(tr("clubSettings.requirePhone", "Telefon"), quality.requirePhone) {
|
||||
quality = quality.copy(requirePhone = it)
|
||||
}
|
||||
RowSwitch(tr("clubSettings.requireEmail", "E-Mail"), quality.requireEmail) {
|
||||
quality = quality.copy(requireEmail = it)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { dependencies.openBackendPath("/club-settings") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp)
|
||||
.heightIn(min = StammdatenTouchMin),
|
||||
) {
|
||||
Text(tr("mobile.openTrainingGroupsWeb", "Trainingsgruppen & Zeiten im Browser"))
|
||||
}
|
||||
|
||||
if (savedHint) {
|
||||
Text(
|
||||
tr("clubSettings.saved", "Gespeichert"),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (perms.canWriteClubSettings()) {
|
||||
scope.launch {
|
||||
saving = true
|
||||
savedHint = false
|
||||
runCatching {
|
||||
dependencies.clubManager.updateClubSettings(
|
||||
clubId,
|
||||
UpdateClubSettingsBody(
|
||||
greetingText = greeting,
|
||||
associationMemberNumber = associationNumber,
|
||||
countryCode = countryCode,
|
||||
stateCode = stateCode.ifBlank { null },
|
||||
myTischtennisFedNickname = myTtNickname.ifBlank { null },
|
||||
autoFetchRankings = autoFetch,
|
||||
memberDataQualityRequirements = normalizeQuality(quality),
|
||||
),
|
||||
)
|
||||
club = dependencies.clubManager.fetchClubDetail(clubId)
|
||||
savedHint = true
|
||||
}.onFailure { loadError = it.message }
|
||||
saving = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !saving && perms.canWriteClubSettings(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp)
|
||||
.heightIn(min = StammdatenTouchMin),
|
||||
) {
|
||||
Text(tr("clubSettings.save", "Speichern"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MobilePredefinedActivitiesScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
val clubState by dependencies.clubManager.state.collectAsState()
|
||||
val clubId = clubState.currentClubId ?: return
|
||||
val perms = clubState.currentPermissions
|
||||
val scope = rememberCoroutineScope()
|
||||
var list by remember { mutableStateOf<List<PredefinedActivityDto>>(emptyList()) }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var refresh by remember { mutableStateOf(0) }
|
||||
var editorTarget by remember { mutableStateOf<PredefinedActivityDto?>(null) }
|
||||
var creatingNew by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(clubId, refresh) {
|
||||
loading = true
|
||||
error = null
|
||||
list = runCatching { dependencies.predefinedActivitiesApi.list(scope = null) }
|
||||
.onFailure { error = it.message }
|
||||
.getOrDefault(emptyList())
|
||||
loading = false
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(StammdatenPad)) {
|
||||
StammdatenTopBar(tr("mobile.predefinedActivities", "Standard-Aktivitäten"), onBack)
|
||||
if (perms?.canReadPredefinedActivities() != true) {
|
||||
Text(tr("mobile.noAccess", "Keine Berechtigung."))
|
||||
return@Column
|
||||
}
|
||||
TextButton(
|
||||
onClick = { dependencies.openBackendPath("/predefined-activities") },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = StammdatenTouchMin),
|
||||
) { Text(tr("mobile.openPredefinedWeb", "Volle Verwaltung (Web)")) }
|
||||
if (perms.canWritePredefinedActivities()) {
|
||||
Button(
|
||||
onClick = { creatingNew = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.heightIn(min = StammdatenTouchMin),
|
||||
) { Text(tr("mobile.newPredefined", "Neue Aktivität")) }
|
||||
}
|
||||
when {
|
||||
loading -> CircularProgressIndicator(modifier = Modifier.padding(top = 24.dp))
|
||||
error != null -> Text(error!!, color = MaterialTheme.colors.error)
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(top = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
items(list, key = { it.id }) { a ->
|
||||
Card(modifier = Modifier.fillMaxWidth(), elevation = 1.dp) {
|
||||
TextButton(
|
||||
onClick = { editorTarget = a },
|
||||
enabled = perms.canWritePredefinedActivities(),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = StammdatenTouchMin),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {
|
||||
Text(a.displayLabel(), fontWeight = FontWeight.SemiBold)
|
||||
a.code?.takeIf { it.isNotBlank() }?.let { Text(it, style = MaterialTheme.typography.caption) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editorTarget != null || creatingNew) {
|
||||
PredefinedActivityEditorDialog(
|
||||
initial = editorTarget,
|
||||
isNew = creatingNew,
|
||||
canSave = perms?.canWritePredefinedActivities() == true,
|
||||
onDismiss = { editorTarget = null; creatingNew = false },
|
||||
onSaved = {
|
||||
editorTarget = null
|
||||
creatingNew = false
|
||||
refresh++
|
||||
},
|
||||
resolve = { k, fb -> tr(k, fb) },
|
||||
scope = scope,
|
||||
api = dependencies.predefinedActivitiesApi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Composable
|
||||
private fun PredefinedActivityEditorDialog(
|
||||
initial: PredefinedActivityDto?,
|
||||
isNew: Boolean,
|
||||
canSave: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSaved: () -> Unit,
|
||||
resolve: (String, String) -> String,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
api: de.tt_tagebuch.shared.api.PredefinedActivitiesApi,
|
||||
) {
|
||||
var name by remember(initial?.id, isNew) { mutableStateOf(initial?.name.orEmpty()) }
|
||||
var code by remember(initial?.id, isNew) { mutableStateOf(initial?.code.orEmpty()) }
|
||||
var description by remember(initial?.id, isNew) { mutableStateOf(initial?.description.orEmpty()) }
|
||||
var durationText by remember(initial?.id, isNew) { mutableStateOf(initial?.durationText.orEmpty()) }
|
||||
var duration by remember(initial?.id, isNew) { mutableStateOf(initial?.duration?.toString().orEmpty()) }
|
||||
var exclude by remember(initial?.id, isNew) { mutableStateOf(initial?.excludeFromStats == true) }
|
||||
var err by remember { mutableStateOf<String?>(null) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(if (isNew) resolve("mobile.newPredefined", "Neue Aktivität") else resolve("mobile.editPredefined", "Bearbeiten")) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
err?.let { Text(it, color = MaterialTheme.colors.error) }
|
||||
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text(resolve("tournaments.name", "Name")) })
|
||||
OutlinedTextField(value = code, onValueChange = { code = it }, label = { Text("Code") })
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(resolve("mobile.description", "Beschreibung")) },
|
||||
minLines = 2,
|
||||
)
|
||||
OutlinedTextField(value = durationText, onValueChange = { durationText = it }, label = { Text("Dauer (Text)") })
|
||||
OutlinedTextField(value = duration, onValueChange = { duration = it.filter { ch -> ch.isDigit() } }, label = { Text("Dauer (Min)") })
|
||||
RowSwitch(resolve("mobile.excludeFromStats", "Von Statistik ausschließen"), exclude) { exclude = it }
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = canSave && !busy && name.isNotBlank(),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
err = null
|
||||
val d = duration.toIntOrNull()
|
||||
val body = PredefinedActivityUpsertBody(
|
||||
name = name.trim(),
|
||||
code = code.trim().ifBlank { null },
|
||||
description = description.trim().ifBlank { null },
|
||||
durationText = durationText.trim().ifBlank { null },
|
||||
duration = d,
|
||||
excludeFromStats = exclude,
|
||||
)
|
||||
runCatching {
|
||||
if (isNew) {
|
||||
api.create(body)
|
||||
} else {
|
||||
api.update(initial!!.id, body)
|
||||
}
|
||||
onSaved()
|
||||
}.onFailure { err = it.message }
|
||||
busy = false
|
||||
}
|
||||
},
|
||||
) { Text(resolve("common.save", "Speichern")) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(resolve("common.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MobileMemberTransferScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
val clubState by dependencies.clubManager.state.collectAsState()
|
||||
val clubId = clubState.currentClubId ?: return
|
||||
val perms = clubState.currentPermissions
|
||||
val scope = rememberCoroutineScope()
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var server by remember { mutableStateOf("") }
|
||||
var transferEndpoint by remember { mutableStateOf("") }
|
||||
var transferMethod by remember { mutableStateOf("POST") }
|
||||
var transferTemplate by remember { mutableStateOf("") }
|
||||
var useBulk by remember { mutableStateOf(false) }
|
||||
var bulkWrap by remember { mutableStateOf("") }
|
||||
var loginUser by remember { mutableStateOf("") }
|
||||
var loginPass by remember { mutableStateOf("") }
|
||||
var hasConfig by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
fun applyFromApi(env: MemberTransferConfigEnvelope?) {
|
||||
val cfg = env?.config
|
||||
if (cfg != null) {
|
||||
hasConfig = true
|
||||
server = cfg.server.orEmpty()
|
||||
transferEndpoint = cfg.transferEndpoint.orEmpty()
|
||||
transferMethod = cfg.transferMethod ?: "POST"
|
||||
transferTemplate = cfg.transferTemplate.orEmpty()
|
||||
useBulk = cfg.useBulkMode == true
|
||||
bulkWrap = cfg.bulkWrapperTemplate.orEmpty()
|
||||
loginUser = cfg.loginCredentials?.username.orEmpty()
|
||||
loginPass = ""
|
||||
} else {
|
||||
hasConfig = false
|
||||
server = ""
|
||||
transferEndpoint = ""
|
||||
transferMethod = "POST"
|
||||
transferTemplate = ""
|
||||
useBulk = false
|
||||
bulkWrap = ""
|
||||
loginUser = ""
|
||||
loginPass = ""
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(clubId) {
|
||||
loading = true
|
||||
error = null
|
||||
val env = runCatching { dependencies.memberTransferConfigApi.get(clubId) }.getOrNull()
|
||||
if (env != null && !env.success) {
|
||||
applyFromApi(null)
|
||||
} else {
|
||||
applyFromApi(env)
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(StammdatenPad),
|
||||
) {
|
||||
StammdatenTopBar(tr("mobile.memberTransfer", "Mitgliedstransfer"), onBack)
|
||||
if (perms?.canReadMembers() != true) {
|
||||
Text(tr("mobile.noAccess", "Keine Berechtigung."))
|
||||
return@Column
|
||||
}
|
||||
TextButton(
|
||||
onClick = { dependencies.openBackendPath("/member-transfer-settings") },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = StammdatenTouchMin),
|
||||
) { Text(tr("mobile.openMemberTransferWeb", "Erweiterte Einstellungen (Web)")) }
|
||||
if (loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(top = 24.dp))
|
||||
return@Column
|
||||
}
|
||||
Text(
|
||||
if (hasConfig) tr("mobile.memberTransferConfigured", "Konfiguration vorhanden") else tr("mobile.memberTransferNotConfigured", "Nicht konfiguriert"),
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
error?.let { Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(top = 8.dp)) }
|
||||
|
||||
if (perms.canWriteMembers()) {
|
||||
OutlinedTextField(
|
||||
value = server,
|
||||
onValueChange = { server = it },
|
||||
label = { Text("Server-URL") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = transferEndpoint,
|
||||
onValueChange = { transferEndpoint = it },
|
||||
label = { Text("Transfer-Endpoint") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = transferMethod,
|
||||
onValueChange = { transferMethod = it },
|
||||
label = { Text("HTTP-Methode") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = transferTemplate,
|
||||
onValueChange = { transferTemplate = it },
|
||||
label = { Text("Transfer-Template") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
minLines = 4,
|
||||
)
|
||||
RowSwitch("Bulk-Modus", useBulk) { useBulk = it }
|
||||
if (useBulk) {
|
||||
OutlinedTextField(
|
||||
value = bulkWrap,
|
||||
onValueChange = { bulkWrap = it },
|
||||
label = { Text("Bulk-Wrapper") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
minLines = 2,
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = loginUser,
|
||||
onValueChange = { loginUser = it },
|
||||
label = { Text("Login Benutzername") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = loginPass,
|
||||
onValueChange = { loginPass = it },
|
||||
label = { Text("Login Passwort (nur bei Änderung)") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
error = null
|
||||
val cred = if (loginUser.isNotBlank() || loginPass.isNotBlank()) {
|
||||
buildJsonObject {
|
||||
if (loginUser.isNotBlank()) put("username", JsonPrimitive(loginUser))
|
||||
if (loginPass.isNotBlank()) put("password", JsonPrimitive(loginPass))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
runCatching {
|
||||
val body = MemberTransferConfigSaveBody(
|
||||
server = server.trim(),
|
||||
loginEndpoint = null,
|
||||
transferEndpoint = transferEndpoint.trim(),
|
||||
transferMethod = transferMethod.trim().ifBlank { "POST" },
|
||||
transferTemplate = transferTemplate,
|
||||
useBulkMode = useBulk,
|
||||
bulkWrapperTemplate = bulkWrap.ifBlank { null },
|
||||
loginCredentials = cred,
|
||||
)
|
||||
val r = dependencies.memberTransferConfigApi.save(clubId, body)
|
||||
if (!r.success) error = r.error ?: r.message ?: "Fehler"
|
||||
else {
|
||||
loginPass = ""
|
||||
applyFromApi(r)
|
||||
}
|
||||
}.onFailure { error = it.message }
|
||||
busy = false
|
||||
}
|
||||
},
|
||||
enabled = !busy && server.isNotBlank() && transferEndpoint.isNotBlank() && transferTemplate.isNotBlank(),
|
||||
modifier = Modifier.weight(1f).heightIn(min = StammdatenTouchMin),
|
||||
) { Text(tr("common.save", "Speichern")) }
|
||||
if (hasConfig) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
error = null
|
||||
runCatching {
|
||||
val r = dependencies.memberTransferConfigApi.delete(clubId)
|
||||
if (r.success) applyFromApi(null)
|
||||
else error = r.error ?: r.message
|
||||
}.onFailure { error = it.message }
|
||||
busy = false
|
||||
}
|
||||
},
|
||||
enabled = !busy,
|
||||
modifier = Modifier.heightIn(min = StammdatenTouchMin),
|
||||
) { Text(tr("common.delete", "Löschen")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,6 @@ internal fun TournamentsScreen(dependencies: AppDependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Composable
|
||||
private fun ModeFilterChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
TextButton(
|
||||
|
||||
@@ -2,9 +2,11 @@ package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.Club
|
||||
import de.tt_tagebuch.shared.api.models.UpdateClubSettingsBody
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -28,6 +30,12 @@ class ClubsApi(
|
||||
return client.http.get("/api/clubs/$clubId").body()
|
||||
}
|
||||
|
||||
suspend fun updateClubSettings(clubId: Int, body: UpdateClubSettingsBody) {
|
||||
client.http.put("/api/clubs/$clubId/settings") {
|
||||
setBody(body)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestAccess(clubId: Int) {
|
||||
client.http.get("/api/clubs/request/$clubId")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.ApiException
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.MemberTransferConfigEnvelope
|
||||
import de.tt_tagebuch.shared.api.models.MemberTransferConfigSaveBody
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
|
||||
class MemberTransferConfigApi(
|
||||
private val client: AuthedHttpClient,
|
||||
) {
|
||||
suspend fun get(clubId: Int): MemberTransferConfigEnvelope? {
|
||||
return try {
|
||||
client.http.get("/api/member-transfer-config/$clubId").body()
|
||||
} catch (e: ApiException) {
|
||||
if (e.statusCode == 404) null else throw e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(clubId: Int, body: MemberTransferConfigSaveBody): MemberTransferConfigEnvelope {
|
||||
return client.http.post("/api/member-transfer-config/$clubId") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun delete(clubId: Int): MemberTransferConfigEnvelope {
|
||||
return client.http.delete("/api/member-transfer-config/$clubId").body()
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@ package de.tt_tagebuch.shared.api
|
||||
|
||||
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
|
||||
import de.tt_tagebuch.shared.api.models.PredefinedActivityDto
|
||||
import de.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
|
||||
class PredefinedActivitiesApi(
|
||||
private val client: AuthedHttpClient,
|
||||
@@ -25,4 +29,16 @@ class PredefinedActivitiesApi(
|
||||
suspend fun getById(id: Int): PredefinedActivityDto {
|
||||
return client.http.get("/api/predefined-activities/$id").body()
|
||||
}
|
||||
|
||||
suspend fun create(body: PredefinedActivityUpsertBody): PredefinedActivityDto {
|
||||
return client.http.post("/api/predefined-activities") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun update(id: Int, body: PredefinedActivityUpsertBody): PredefinedActivityDto {
|
||||
return client.http.put("/api/predefined-activities/$id") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,26 @@ package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MemberDataQualityRequirements(
|
||||
val requireStreet: Boolean = true,
|
||||
val requirePostalCode: Boolean = true,
|
||||
val requireCity: Boolean = true,
|
||||
val requirePhone: Boolean = true,
|
||||
val requireEmail: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateClubSettingsBody(
|
||||
val greetingText: String? = null,
|
||||
val associationMemberNumber: String? = null,
|
||||
val countryCode: String? = null,
|
||||
val stateCode: String? = null,
|
||||
val myTischtennisFedNickname: String? = null,
|
||||
val autoFetchRankings: Boolean? = null,
|
||||
val memberDataQualityRequirements: MemberDataQualityRequirements? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Club(
|
||||
val id: Int,
|
||||
@@ -10,5 +30,8 @@ data class Club(
|
||||
val associationMemberNumber: String? = null,
|
||||
val myTischtennisFedNickname: String? = null,
|
||||
val autoFetchRankings: Boolean? = null,
|
||||
val countryCode: String? = null,
|
||||
val stateCode: String? = null,
|
||||
val memberDataQualityRequirements: MemberDataQualityRequirements? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,3 +81,23 @@ fun UserClubPermissions.canWriteTournaments(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("tournaments", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadClubSettings(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("settings", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWriteClubSettings(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("settings", "write")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canReadPredefinedActivities(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("predefined_activities", "read")
|
||||
}
|
||||
|
||||
fun UserClubPermissions.canWritePredefinedActivities(): Boolean {
|
||||
if (isOwner) return true
|
||||
return permissions.boolAt("predefined_activities", "write")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.tt_tagebuch.shared.api.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class MemberTransferLoginCredentialsDto(
|
||||
val username: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberTransferConfigDto(
|
||||
val id: Int? = null,
|
||||
val clubId: Int? = null,
|
||||
val server: String? = null,
|
||||
val loginEndpoint: String? = null,
|
||||
val loginFormat: String? = null,
|
||||
val loginCredentials: MemberTransferLoginCredentialsDto? = null,
|
||||
val transferEndpoint: String? = null,
|
||||
val transferMethod: String? = null,
|
||||
val transferFormat: String? = null,
|
||||
val transferTemplate: String? = null,
|
||||
val useBulkMode: Boolean? = null,
|
||||
val bulkWrapperTemplate: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberTransferConfigEnvelope(
|
||||
val success: Boolean = false,
|
||||
val config: MemberTransferConfigDto? = null,
|
||||
val message: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberTransferConfigSaveBody(
|
||||
val server: String,
|
||||
val loginEndpoint: String? = null,
|
||||
val loginFormat: String = "json",
|
||||
val loginCredentials: JsonObject? = null,
|
||||
val transferEndpoint: String,
|
||||
val transferMethod: String = "POST",
|
||||
val transferFormat: String = "json",
|
||||
val transferTemplate: String,
|
||||
val useBulkMode: Boolean = false,
|
||||
val bulkWrapperTemplate: String? = null,
|
||||
)
|
||||
@@ -13,6 +13,18 @@ data class PredefinedActivityDto(
|
||||
val excludeFromStats: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PredefinedActivityUpsertBody(
|
||||
val name: String? = null,
|
||||
val code: String? = null,
|
||||
val description: String? = null,
|
||||
val duration: Int? = null,
|
||||
val durationText: String? = null,
|
||||
val imageLink: String? = null,
|
||||
val drawingData: String? = null,
|
||||
val excludeFromStats: Boolean? = null,
|
||||
)
|
||||
|
||||
fun PredefinedActivityDto.displayLabel(): String {
|
||||
val n = name?.trim().orEmpty()
|
||||
if (n.isNotEmpty()) return n
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.tt_tagebuch.shared.state
|
||||
import de.tt_tagebuch.shared.api.ClubsApi
|
||||
import de.tt_tagebuch.shared.api.PermissionsApi
|
||||
import de.tt_tagebuch.shared.api.models.Club
|
||||
import de.tt_tagebuch.shared.api.models.UpdateClubSettingsBody
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -50,6 +51,10 @@ class ClubManager(
|
||||
return clubsApi.getClub(clubId)
|
||||
}
|
||||
|
||||
suspend fun updateClubSettings(clubId: Int, body: UpdateClubSettingsBody) {
|
||||
clubsApi.updateClubSettings(clubId, body)
|
||||
}
|
||||
|
||||
suspend fun selectClub(clubId: Int) {
|
||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user