diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt index 10b0703..414cbd1 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt @@ -84,10 +84,11 @@ class DriverRepository( type: String, requestMode: String, requestedQuantity: Int?, + details: Map, ): DriverLeaveForecastDto = api.leaveForecast( authHeader(requireToken()), - LeaveForecastBody(type, dateFrom, dateTo, requestMode, requestedQuantity), + LeaveForecastBody(type, dateFrom, dateTo, requestMode, requestedQuantity, details), ).data suspend fun leaveCalendar(from: String, to: String): List = diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt index 184124c..0fa2a09 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt @@ -236,6 +236,10 @@ data class LeaveRequestTypeDto( val description: String? = null, val decisionMode: String? = null, val payrollMode: String? = null, + val financialTreatmentLabel: String? = null, + val affectsAnnualLeaveBalance: Boolean = false, + val annualEntitlementImpact: String? = null, + val requiresHrVerification: List = emptyList(), val legalBasis: String? = null, ) @@ -279,6 +283,7 @@ data class LeaveForecastBody( val dateTo: String, val requestMode: String, val requestedQuantity: Int? = null, + val details: Map = emptyMap(), ) data class LeaveForecastResponse( @@ -287,6 +292,11 @@ data class LeaveForecastResponse( data class DriverLeaveForecastDto( val ruleCode: String, + val payrollMode: String? = null, + val financialTreatmentLabel: String? = null, + val affectsAnnualLeaveBalance: Boolean = false, + val annualEntitlementImpact: String? = null, + val requiresHrVerification: List = emptyList(), val quantityUnit: String, val days: List = emptyList(), val workingDaysCount: Int = 0, @@ -329,6 +339,7 @@ data class DriverLeaveRequestDto( val typeLabel: String? = null, val requestMode: String = "days", val requestedQuantity: Int? = null, + val quantityUnit: String? = null, val rule: DriverLeaveRuleSnapshotDto? = null, val details: Map = emptyMap(), val note: String? = null, @@ -344,6 +355,13 @@ data class DriverLeaveRequestDto( val allocations: List = emptyList(), val balanceConfigurationRequired: Boolean = false, val missingDocumentTypes: List = emptyList(), + val payrollMode: String? = null, + val payrollItemTypes: List = emptyList(), + val financialTreatmentLabel: String? = null, + val affectsAnnualLeaveBalance: Boolean = false, + val annualEntitlementImpact: String? = null, + val requiresLegalVerification: Boolean = false, + val legalVerificationChecks: List = emptyList(), ) data class DriverLeaveRuleSnapshotDto( @@ -363,6 +381,10 @@ data class DriverLeaveRuleRequirementsDto( val requiredDocuments: List = emptyList(), val fieldOptions: Map> = emptyMap(), val description: String? = null, + val financialTreatmentLabel: String? = null, + val affectsAnnualLeaveBalance: Boolean = false, + val annualEntitlementImpact: String? = null, + val requiresHrVerification: List = emptyList(), ) data class DriverLeaveAllocationDto( diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index 165acc5..76e24ee 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -1523,7 +1523,11 @@ private fun LeaveRequestsScreen( item { EmptyState("Nie masz jeszcze żadnych wniosków") } } else { items(state.leaveRequests, key = { it.id }) { request -> - LeaveRequestCard(request, onClick = { onOpen(request.id) }) + LeaveRequestCard( + request = request, + dailyNormMinutes = state.leaveSummary?.dailyNormMinutes ?: 480, + onClick = { onOpen(request.id) }, + ) } } } @@ -1634,6 +1638,56 @@ private fun LeaveBalanceOverview(state: DriverUiState) { ) } } + + val otherPools = DriverLeaveRequestUiRules.otherEntitlementPools(summary) + if (otherPools.isNotEmpty()) { + HorizontalDivider(color = TppTheme.colors.outline) + Text( + "Pozostałe dostępne limity", + color = TppTheme.colors.ink, + fontWeight = FontWeight.Bold, + fontSize = 17.sp, + ) + Text( + "Te limity są rozliczane osobno i nie zmniejszają zwykłego urlopu wypoczynkowego.", + color = TppTheme.colors.muted, + fontSize = 13.sp, + ) + otherPools.forEach { pool -> + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + "${DriverLeaveRequestUiRules.poolLabel(pool.poolCode)} · ${pool.year}", + color = TppTheme.colors.muted, + modifier = Modifier.weight(1f), + ) + Text( + DriverLeaveRequestUiRules.formatQuantity(pool.availableQuantity, pool.unit, norm), + color = TppTheme.colors.ink, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End, + ) + } + if (pool.usedQuantity > 0 || pool.reservedQuantity > 0) { + Text( + buildString { + if (pool.usedQuantity > 0) { + append("Wykorzystano ") + append(DriverLeaveRequestUiRules.formatQuantity(pool.usedQuantity, pool.unit, norm)) + } + if (pool.reservedQuantity > 0) { + if (isNotEmpty()) append(" · ") + append("W przyszłych wnioskach ") + append(DriverLeaveRequestUiRules.formatQuantity(pool.reservedQuantity, pool.unit, norm)) + } + }, + color = TppTheme.colors.muted, + fontSize = 12.sp, + ) + } + } + } + } } } } @@ -1653,7 +1707,11 @@ private fun LeaveBalanceStat(title: String, value: String, modifier: Modifier = } @Composable -private fun LeaveRequestCard(request: DriverLeaveRequestDto, onClick: () -> Unit) { +private fun LeaveRequestCard( + request: DriverLeaveRequestDto, + dailyNormMinutes: Int, + onClick: () -> Unit, +) { Card( colors = CardDefaults.cardColors(containerColor = TppTheme.colors.card), border = BorderStroke(1.dp, TppTheme.colors.outline), @@ -1670,6 +1728,27 @@ private fun LeaveRequestCard(request: DriverLeaveRequestDto, onClick: () -> Unit fontSize = 17.sp, ) Text(leaveDateRange(request.dateFrom, request.dateTo), color = TppTheme.colors.muted, fontSize = 14.sp) + request.requestedQuantity?.let { quantity -> + Text( + DriverLeaveRequestUiRules.formatQuantity( + quantity, + DriverLeaveRequestUiRules.requestQuantityUnit(request), + dailyNormMinutes, + ), + color = TppTheme.colors.ink, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } + Text( + if (request.affectsAnnualLeaveBalance) { + "Pomniejsza pulę urlopu wypoczynkowego" + } else { + "Nie pomniejsza puli urlopu wypoczynkowego" + }, + color = if (request.affectsAnnualLeaveBalance) TppTheme.colors.forest else TppTheme.colors.muted, + fontSize = 12.sp, + ) if (!request.note.isNullOrBlank()) { Text(request.note, color = TppTheme.colors.muted, maxLines = 2, overflow = TextOverflow.Ellipsis) } @@ -1732,20 +1811,36 @@ private fun LeaveRequestDetailScreen( } DetailLine("Wybrany termin", leaveDateRange(request.dateFrom, request.dateTo)) request.requestedQuantity?.let { quantity -> - val unit = when { - request.requestMode == "hours" -> "minutes" - request.rule?.unit == "calendar_days" -> "days" - else -> "minutes" - } DetailLine( "Czas nieobecności", DriverLeaveRequestUiRules.formatQuantity( quantity, - unit, + DriverLeaveRequestUiRules.requestQuantityUnit(request), state.leaveSummary?.dailyNormMinutes ?: 480, ), ) } + DetailLine( + "Wpływ na urlop wypoczynkowy", + if (request.affectsAnnualLeaveBalance) { + "Pomniejsza odpowiednią pulę urlopu" + } else { + "Nie pomniejsza puli urlopu wypoczynkowego" + }, + ) + if (!request.financialTreatmentLabel.isNullOrBlank()) { + DetailLine("Sposób rozliczenia", request.financialTreatmentLabel) + } + DriverLeaveRequestUiRules.annualEntitlementImpact(request.annualEntitlementImpact)?.let { + Text(it, color = Color(0xFF7A5A00), fontSize = 13.sp) + } + if (request.requiresLegalVerification) { + Text( + "Kadry sprawdzą wymagane warunki i dokumenty przed ostatecznym rozliczeniem.", + color = Color(0xFF7A5A00), + fontSize = 13.sp, + ) + } DetailLine("Wysłano", request.submittedAt?.let(::shortDateTime) ?: "-") if (request.decidedAt != null) { DetailLine("Decyzję zapisano", shortDateTime(request.decidedAt)) @@ -1783,6 +1878,32 @@ private fun LeaveRequestDetailScreen( } } } + val submittedDetails = request.details + .filterValues { it.isNotBlank() } + .toList() + if (submittedDetails.isNotEmpty()) { + item { + Text("Podane informacje", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 18.sp) + } + item { + Card( + colors = CardDefaults.cardColors(containerColor = TppTheme.colors.card), + border = BorderStroke(1.dp, TppTheme.colors.outline), + shape = MaterialTheme.shapes.medium, + ) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + submittedDetails.forEach { (field, rawValue) -> + val displayedValue = request.rule?.rule + ?.fieldOptions + ?.get(field) + ?.get(rawValue) + ?: rawValue + DetailLine(DriverLeaveRequestUiRules.fieldLabel(field), displayedValue) + } + } + } + } + } val requiredDocuments = request.rule?.rule?.requiredDocuments.orEmpty() if (requiredDocuments.isNotEmpty()) { item { @@ -1836,7 +1957,7 @@ private fun LeaveRequestDetailScreen( } if (request.allocations.isNotEmpty()) { item { - Text("Jak rozliczono urlop", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 18.sp) + Text("Jak rozliczono nieobecność", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 18.sp) } item { Card( @@ -1845,26 +1966,35 @@ private fun LeaveRequestDetailScreen( shape = MaterialTheme.shapes.medium, ) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - request.allocations.groupBy { it.sourceYear }.forEach { (year, allocations) -> - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - if (year != null) "Z puli $year" else "Rozliczenie", - color = TppTheme.colors.muted, - ) - Text( - DriverLeaveRequestUiRules.formatQuantity( - allocations.sumOf { it.quantity }, - if (request.rule?.unit == "calendar_days") "days" else "minutes", - state.leaveSummary?.dailyNormMinutes ?: 480, - ), - color = TppTheme.colors.ink, - fontWeight = FontWeight.Bold, - ) + request.allocations + .groupBy { it.sourceYear to it.status } + .forEach { (source, allocations) -> + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + source.first?.let { "Z puli $it" } ?: "Czas nieobecności", + color = TppTheme.colors.muted, + ) + Text( + DriverLeaveRequestUiRules.formatQuantity( + allocations.sumOf { it.quantity }, + DriverLeaveRequestUiRules.requestQuantityUnit(request), + state.leaveSummary?.dailyNormMinutes ?: 480, + ), + color = TppTheme.colors.ink, + fontWeight = FontWeight.Bold, + ) + } + Text( + DriverLeaveRequestUiRules.allocationStatusLabel(source.second), + color = TppTheme.colors.muted, + fontSize = 12.sp, + ) + } } } } } - } } item { Text("Co działo się z wnioskiem", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 18.sp) @@ -2328,7 +2458,7 @@ private fun AddLeaveRequestScreen( Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { Text("Złóż wniosek krok po kroku", color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, fontSize = 23.sp) Text( - "Wybierz rodzaj, termin i sprawdź podsumowanie. Aplikacja policzy urlop za Ciebie.", + "Wybierz rodzaj i termin. Aplikacja policzy dni lub godziny oraz pokaże wpływ na saldo.", color = TppTheme.colors.muted, ) } @@ -2341,32 +2471,59 @@ private fun AddLeaveRequestScreen( color = Color(0xFFB91C1C), ) } else { - availableTypes.forEach { type -> - val active = type.value == state.leaveRequestType - Card( - colors = CardDefaults.cardColors( - containerColor = if (active) TppTheme.colors.successContainer else Color.White, - ), - border = BorderStroke( - 1.dp, - if (active) TppTheme.colors.forest else TppTheme.colors.outline, - ), - shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth().clickable { onType(type.value) }, - ) { - Column(Modifier.padding(13.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(type.label, color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) - if (active) { - Icon(Icons.Outlined.CheckCircle, contentDescription = "Wybrano", tint = TppTheme.colors.forest) + availableTypes.groupBy { it.category }.forEach { (category, types) -> + Text( + DriverLeaveRequestUiRules.categoryLabel(category), + color = TppTheme.colors.muted, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + modifier = Modifier.padding(top = 4.dp), + ) + types.forEach { type -> + val active = type.value == state.leaveRequestType + Card( + colors = CardDefaults.cardColors( + containerColor = if (active) TppTheme.colors.successContainer else Color.White, + ), + border = BorderStroke( + 1.dp, + if (active) TppTheme.colors.forest else TppTheme.colors.outline, + ), + shape = MaterialTheme.shapes.medium, + modifier = Modifier.fillMaxWidth().clickable { onType(type.value) }, + ) { + Column(Modifier.padding(13.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(type.label, color = TppTheme.colors.ink, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) + if (active) { + Icon(Icons.Outlined.CheckCircle, contentDescription = "Wybrano", tint = TppTheme.colors.forest) + } + } + if (!type.description.isNullOrBlank()) { + Text(type.description, color = TppTheme.colors.muted, fontSize = 13.sp) + } + Text( + if (type.affectsAnnualLeaveBalance) { + "Pomniejsza odpowiednią pulę urlopu wypoczynkowego." + } else { + "Nie pomniejsza puli urlopu wypoczynkowego." + }, + color = if (type.affectsAnnualLeaveBalance) TppTheme.colors.forest else TppTheme.colors.muted, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + ) + DriverLeaveRequestUiRules.annualEntitlementImpact(type.annualEntitlementImpact)?.let { + Text( + it, + color = Color(0xFF7A5A00), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + ) } - } - if (!type.description.isNullOrBlank()) { - Text(type.description, color = TppTheme.colors.muted, fontSize = 13.sp) } } } @@ -2375,7 +2532,7 @@ private fun AddLeaveRequestScreen( } } item { - GuidedStepCard(number = "2", title = "Kiedy potrzebujesz wolnego?") { + GuidedStepCard(number = "2", title = "Kiedy ma trwać nieobecność?") { Card( colors = CardDefaults.cardColors(containerColor = Color.White), border = BorderStroke( @@ -2470,7 +2627,7 @@ private fun AddLeaveRequestScreen( if (active) Icon(Icons.Outlined.CheckCircle, contentDescription = "Wybrano") } } - } else if (field.endsWith("Date")) { + } else if (field.endsWith("Date") || field.endsWith("At")) { Card( colors = CardDefaults.cardColors(containerColor = Color.White), border = BorderStroke(1.dp, TppTheme.colors.outline), @@ -2501,6 +2658,13 @@ private fun AddLeaveRequestScreen( supportingText = DriverLeaveRequestUiRules.fieldHint(field)?.let { hint -> { Text(hint) } }, + keyboardOptions = KeyboardOptions( + keyboardType = if (field in setOf("eligibleHospitalDays", "partNumber")) { + KeyboardType.Number + } else { + KeyboardType.Text + }, + ), modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, ) @@ -2536,6 +2700,28 @@ private fun AddLeaveRequestScreen( color = TppTheme.colors.muted, fontSize = 13.sp, ) + if (!selectedType.financialTreatmentLabel.isNullOrBlank()) { + Text( + "Rozliczenie: ${selectedType.financialTreatmentLabel}", + color = TppTheme.colors.ink, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } + DriverLeaveRequestUiRules.annualEntitlementImpact(selectedType.annualEntitlementImpact)?.let { + Text( + it, + color = Color(0xFF7A5A00), + fontSize = 13.sp, + ) + } + if (selectedType.requiresHrVerification.isNotEmpty()) { + Text( + "Kadry sprawdzą indywidualne warunki i dokumenty przed zatwierdzeniem. Nie musisz samodzielnie obliczać limitu ani kwoty.", + color = Color(0xFF7A5A00), + fontSize = 13.sp, + ) + } } } } @@ -2585,6 +2771,16 @@ private fun AddLeaveRequestScreen( ) }", ) + Text( + if (state.leaveForecast.affectsAnnualLeaveBalance) { + "Ten wniosek pomniejszy odpowiednią pulę urlopu wypoczynkowego." + } else { + "Ten wniosek nie pomniejszy puli urlopu wypoczynkowego." + }, + ) + if (!state.leaveForecast.financialTreatmentLabel.isNullOrBlank()) { + Text("Sposób rozliczenia: ${state.leaveForecast.financialTreatmentLabel}") + } Text("Po wysłaniu zobaczysz status oraz dalsze wymagane kroki.") } }, @@ -2659,13 +2855,17 @@ private fun LeaveForecastCard(state: DriverUiState, forecast: pl.firmatpp.kierow ) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - if (success) "Wniosek jest gotowy do wysłania" else "Za mało urlopu do wykorzystania", + if (success) "Dane są gotowe do wysłania" else "Za mało dostępnego limitu", color = if (success) TppTheme.colors.forest else Color(0xFFB91C1C), fontWeight = FontWeight.Bold, fontSize = 18.sp, ) DetailLine( - "Ten wniosek wykorzysta", + if (forecast.affectsAnnualLeaveBalance) { + "Pomniejszy pulę urlopu o" + } else { + "Czas tej nieobecności" + }, DriverLeaveRequestUiRules.formatQuantity( forecast.requestedQuantity, forecast.quantityUnit, @@ -2675,6 +2875,35 @@ private fun LeaveForecastCard(state: DriverUiState, forecast: pl.firmatpp.kierow if (forecast.workingDaysCount > 0) { DetailLine("Dni pracy w wybranym terminie", DriverLeaveRequestUiRules.dayCountLabel(forecast.workingDaysCount)) } + if (!forecast.affectsAnnualLeaveBalance) { + Text( + "Ta nieobecność nie zmniejszy liczby dni urlopu wypoczynkowego.", + color = TppTheme.colors.muted, + fontSize = 13.sp, + ) + } + if (!forecast.financialTreatmentLabel.isNullOrBlank()) { + Text( + "Rozliczenie: ${forecast.financialTreatmentLabel}", + color = TppTheme.colors.ink, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } + DriverLeaveRequestUiRules.annualEntitlementImpact(forecast.annualEntitlementImpact)?.let { + Text( + it, + color = Color(0xFF7A5A00), + fontSize = 13.sp, + ) + } + if (forecast.requiresHrVerification.isNotEmpty()) { + Text( + "Przed zatwierdzeniem kadry potwierdzą ustawowe warunki tego uprawnienia.", + color = Color(0xFF7A5A00), + fontSize = 13.sp, + ) + } if (!forecast.balanceConfigured) { Text( "Saldo nie jest jeszcze potwierdzone. Wniosek zostanie zapisany, ale nie będzie mógł zostać zaakceptowany przed konfiguracją przez kadry.", diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRules.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRules.kt index e632811..ce0467f 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRules.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRules.kt @@ -47,6 +47,7 @@ object DriverLeaveRequestUiRules { "cancellation_rejected" -> "Anulowanie odrzucone" "period_amended" -> "Zmieniono termin wniosku" "interrupted" -> "Nieobecność została przerwana" + "annual_leave_interrupted" -> "Przywrócono urlop wypoczynkowy" else -> "Aktualizacja wniosku" } @@ -77,6 +78,8 @@ object DriverLeaveRequestUiRules { "AUTHORITY_SUMMONS" -> "Wezwanie urzędowe" "RESCUE_DUTIES" -> "Obowiązki ratownicze" "MILITARY_DUTIES" -> "Obowiązki wojskowe" + "ORGAN_DONOR_EXAMS" -> "Badania dawcy" + "CARE_SICKNESS" -> "Opieka nad chorym dzieckiem lub członkiem rodziny" else -> type } @@ -103,6 +106,60 @@ object DriverLeaveRequestUiRules { .filter { it.poolCode == "ANNUAL" && it.status == "active" } .sortedByDescending { it.year } + fun otherEntitlementPools(summary: DriverLeaveSummaryDto?): List = + summary?.pools.orEmpty() + .filter { it.poolCode != "ANNUAL" && it.status == "active" } + .sortedWith(compareBy({ poolLabel(it.poolCode) }, { -it.year })) + + fun poolLabel(poolCode: String): String = + when (poolCode) { + "ANNUAL" -> "Urlop wypoczynkowy" + "ANNUAL_DISABILITY" -> "Dodatkowy urlop wypoczynkowy" + "FORCE_MAJEURE" -> "Siła wyższa" + "CHILD_CARE_14" -> "Opieka nad dzieckiem do 14 lat" + "CARE_LEAVE" -> "Urlop opiekuńczy" + else -> typeLabel(poolCode) + } + + fun requestQuantityUnit(request: DriverLeaveRequestDto): String { + request.quantityUnit?.takeIf { it == "days" || it == "minutes" }?.let { return it } + if (request.requestMode == "hours") return "minutes" + if (request.rule?.unit == "calendar_days") return "days" + + val poolCode = request.rule?.poolCode + return if ( + request.requestMode == "days" && + poolCode != null && + poolCode !in setOf("ANNUAL", "ANNUAL_DISABILITY") + ) { + "days" + } else { + "minutes" + } + } + + fun allocationStatusLabel(status: String): String = + when (status) { + "reserved" -> "Uwzględniono w przyszłej nieobecności" + "used" -> "Wykorzystano" + "released" -> "Przywrócono do wykorzystania" + else -> "Rozliczono" + } + + fun categoryLabel(category: String?): String = + when (category) { + "annual" -> "Urlop wypoczynkowy" + "statutory_short" -> "Krótkie uprawnienia ustawowe" + "family" -> "Rodzina i opieka" + "parental" -> "Rodzicielstwo" + "health" -> "Zdrowie i rehabilitacja" + "training" -> "Nauka i szkolenia" + "statutory_exemption" -> "Pozostałe zwolnienia ustawowe" + "unpaid" -> "Nieobecności bezpłatne" + "operational" -> "Pozostałe nieobecności" + else -> "Inne rodzaje nieobecności" + } + fun formatQuantity(quantity: Int, unit: String, dailyNormMinutes: Int = 480): String { if (unit == "days") return dayCountLabel(quantity) @@ -149,6 +206,15 @@ object DriverLeaveRequestUiRules { else -> "Wniosek zostanie przekazany do osoby, która podejmie decyzję." } + fun annualEntitlementImpact(impact: String?): String? = + when (impact) { + "proportional_after_one_month" -> + "Nie pomniejszy puli od razu, ale okres trwający co najmniej miesiąc może zmniejszyć roczny wymiar urlopu po powrocie do pracy." + "proportional_on_return_if_started_before_year" -> + "Po powrocie z urlopu rozpoczętego w poprzednim roku wymiar urlopu wypoczynkowego zostanie naliczony proporcjonalnie." + else -> null + } + fun fieldLabel(field: String): String = when (field) { "childBirthDate" -> "Data urodzenia dziecka" @@ -165,6 +231,16 @@ object DriverLeaveRequestUiRules { "examType" -> "Rodzaj egzaminu lub nauki" "educationProgramKey" -> "Nazwa szkoły lub programu" "classification" -> "Czego dotyczy nieobecność" + "urgentFamilyReason" -> "Jaka pilna sprawa rodzinna wymaga Twojej obecności?" + "childrenPlacedCount" -> "Liczba dzieci przyjętych na wychowanie" + "placementType" -> "Sposób przyjęcia dziecka na wychowanie" + "hospitalizationGroup" -> "Która sytuacja hospitalizacji dotyczy dziecka?" + "eligibleHospitalDays" -> "Liczba dni hospitalizacji potwierdzona przez szpital" + "maternityLeaveEndsAt" -> "Data zakończenia urlopu macierzyńskiego" + "parentalEntitlementVariant" -> "Wymiar urlopu rodzicielskiego" + "childcareEntitlementVariant" -> "Rodzaj urlopu wychowawczego" + "careRecipientType" -> "Kogo dotyczy opieka?" + "careEntitlementEventKey" -> "Opis okresu opieki" else -> "Dodatkowa informacja" } @@ -176,6 +252,9 @@ object DriverLeaveRequestUiRules { "childIdentityKey" -> "Wystarczy imię i rok urodzenia" "educationProgramKey" -> "Np. Technikum Transportowe w Poznaniu" "classification" -> "Krótko opisz powód, aby kadry mogły go prawidłowo zakwalifikować" + "urgentFamilyReason" -> "Krótko opisz chorobę lub wypadek oraz dlaczego Twoja natychmiastowa obecność jest konieczna." + "eligibleHospitalDays" -> "Wpisz liczbę z zaświadczenia, np. 12" + "careEntitlementEventKey" -> "Wpisz prosty opis pozwalający kadrom połączyć części tego samego uprawnienia." else -> null } @@ -196,6 +275,9 @@ object DriverLeaveRequestUiRules { "rescue_service_confirmation" -> "Potwierdzenie służby ratowniczej" "military_document" -> "Dokument wojskowy" "external_sickness_confirmation" -> "Potwierdzenie niezdolności do pracy" + "other_parent_declaration" -> "Oświadczenie drugiego rodzica" + "organ_donor_confirmation" -> "Potwierdzenie badań lub zabiegu dawcy" + "external_care_confirmation" -> "Potwierdzenie opieki lub e-ZLA" else -> "Wymagany dokument" } diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt index f65289b..eb65e0f 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -1259,6 +1259,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) type = snapshot.leaveRequestType, requestMode = snapshot.leaveRequestMode, requestedQuantity = requestedMinutes, + details = snapshot.leaveRequestDetails, ) }.onSuccess { forecast -> _state.update { it.copy(leaveForecast = forecast, error = null) } diff --git a/app/src/test/java/pl/firmatpp/kierowca/data/DriverLeaveApiContractTest.kt b/app/src/test/java/pl/firmatpp/kierowca/data/DriverLeaveApiContractTest.kt index f5d7cc8..b3a1f47 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/data/DriverLeaveApiContractTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/data/DriverLeaveApiContractTest.kt @@ -38,7 +38,9 @@ class DriverLeaveApiContractTest { "requiredFields": [], "requiredDocuments": [], "fieldOptions": {}, - "description": "Urlop rozliczany według grafiku." + "description": "Urlop rozliczany według grafiku.", + "financialTreatmentLabel": "Płatny według danych przekazanych przez kadry i płace.", + "affectsAnnualLeaveBalance": true }] } } @@ -51,6 +53,7 @@ class DriverLeaveApiContractTest { assertEquals("URLOP", response.data.availableTypes.single().value) assertEquals("ANNUAL", response.data.availableTypes.single().code) assertTrue("hours" in response.data.availableTypes.single().requestModes) + assertTrue(response.data.availableTypes.single().affectsAnnualLeaveBalance) } @Test @@ -64,11 +67,14 @@ class DriverLeaveApiContractTest { "type": "WOLNE", "ruleCode": "CHILD_CARE_14", "requestMode": "days", - "requestedQuantity": 480, + "requestedQuantity": 1, + "quantityUnit": "days", "details": { "childrenBornCount": 1, "childBirthDate": "2020-05-12" }, + "financialTreatmentLabel": "Płatne według zasad ustawowych.", + "affectsAnnualLeaveBalance": false, "status": "pending", "missingDocumentTypes": ["parent_declaration"] } @@ -78,6 +84,9 @@ class DriverLeaveApiContractTest { assertEquals("1", request.details["childrenBornCount"]) assertEquals("2020-05-12", request.details["childBirthDate"]) + assertEquals("days", request.quantityUnit) + assertEquals(false, request.affectsAnnualLeaveBalance) + assertEquals("Płatne według zasad ustawowych.", request.financialTreatmentLabel) assertEquals(listOf("parent_declaration"), request.missingDocumentTypes) } } diff --git a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRulesTest.kt b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRulesTest.kt index fd8b179..22b3b84 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRulesTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverLeaveRequestUiRulesTest.kt @@ -8,6 +8,8 @@ import org.junit.Test import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarDriverDto import pl.firmatpp.kierowca.data.model.DriverLeaveCalendarEntryDto import pl.firmatpp.kierowca.data.model.DriverLeavePoolDto +import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto +import pl.firmatpp.kierowca.data.model.DriverLeaveRuleSnapshotDto import pl.firmatpp.kierowca.data.model.DriverLeaveSummaryDto class DriverLeaveRequestUiRulesTest { @@ -59,9 +61,71 @@ class DriverLeaveRequestUiRulesTest { assertEquals("Kierowca poprosił o anulowanie", DriverLeaveRequestUiRules.eventActionLabel("cancellation_requested")) assertEquals("Anulowanie zatwierdzone", DriverLeaveRequestUiRules.eventActionLabel("cancellation_approved")) assertEquals("Anulowanie odrzucone", DriverLeaveRequestUiRules.eventActionLabel("cancellation_rejected")) + assertEquals("Przywrócono urlop wypoczynkowy", DriverLeaveRequestUiRules.eventActionLabel("annual_leave_interrupted")) assertEquals("Aktualizacja wniosku", DriverLeaveRequestUiRules.eventActionLabel("some_raw_action")) } + @Test + fun formatsStatutoryDayLimitsAsDaysInsteadOfMinutes() { + val dayBasedRequest = DriverLeaveRequestDto( + id = "child-care", + dateFrom = "2026-08-03", + dateTo = "2026-08-03", + type = "CHILD_CARE_14", + requestMode = "days", + requestedQuantity = 1, + quantityUnit = "days", + status = "pending", + ) + val legacyDayBasedRequest = dayBasedRequest.copy( + id = "force-majeure", + type = "FORCE_MAJEURE", + quantityUnit = null, + rule = DriverLeaveRuleSnapshotDto( + unit = "work_schedule", + poolCode = "FORCE_MAJEURE", + ), + ) + + assertEquals("days", DriverLeaveRequestUiRules.requestQuantityUnit(dayBasedRequest)) + assertEquals("1 dzień", DriverLeaveRequestUiRules.formatQuantity(1, "days")) + assertEquals("days", DriverLeaveRequestUiRules.requestQuantityUnit(legacyDayBasedRequest)) + } + + @Test + fun exposesOtherStatutoryBalancesSeparatelyFromAnnualLeave() { + val summary = DriverLeaveSummaryDto( + pools = listOf( + annualPool(year = 2026, available = 4_800, reserved = 0, used = 0, granted = 4_800), + DriverLeavePoolDto( + id = 31, + poolCode = "CHILD_CARE_14", + year = 2026, + unit = "days", + availableQuantity = 1, + usedQuantity = 1, + grantedQuantity = 2, + status = "active", + ), + DriverLeavePoolDto( + id = 32, + poolCode = "CARE_LEAVE", + year = 2026, + unit = "days", + availableQuantity = 5, + grantedQuantity = 5, + status = "active", + ), + ), + ) + + val pools = DriverLeaveRequestUiRules.otherEntitlementPools(summary) + + assertEquals(listOf("CHILD_CARE_14", "CARE_LEAVE"), pools.map { it.poolCode }) + assertEquals("Urlop opiekuńczy", DriverLeaveRequestUiRules.poolLabel("CARE_LEAVE")) + assertEquals("Rodzina i opieka", DriverLeaveRequestUiRules.categoryLabel("family")) + } + @Test fun driverCanCancelPendingAndFutureApprovedRequests() { val tomorrow = LocalDate.now().plusDays(1).toString()