From 13ed7d911e962287c4131272d73553be0f88291b Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 25 Mar 2026 15:39:49 +0100 Subject: [PATCH] fix: align API models with backend and add inline position pickers - Fix SchedulerModels field mapping (camelCase, move transport_code/contractor_route to top-level) - Add inline position picker pills inside category cards to match Next.js external service form Co-Authored-By: Claude Opus 4.6 (1M context) --- .../data/model/SchedulerModels.kt | 13 +- .../data/repository/SchedulerRepository.kt | 6 +- .../ui/service/wizard/ServiceSelectStep.kt | 183 +++++++++++------- 3 files changed, 119 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/pl/firmatpp/itstransport/data/model/SchedulerModels.kt b/app/src/main/java/pl/firmatpp/itstransport/data/model/SchedulerModels.kt index 705f510..aea6fe7 100644 --- a/app/src/main/java/pl/firmatpp/itstransport/data/model/SchedulerModels.kt +++ b/app/src/main/java/pl/firmatpp/itstransport/data/model/SchedulerModels.kt @@ -11,8 +11,9 @@ data class SchedulerEventResponse( val title: String, @SerializedName("resourceId") val resourceId: String, val start: String, - val end: String, + val end: String? = null, val color: String? = null, + val description: String? = null, @SerializedName("extendedProps") val extendedProps: EventExtendedProps, ) @@ -21,14 +22,12 @@ data class SchedulerEventResponse( * Contains the transport-specific fields displayed on route cards. */ data class EventExtendedProps( - @SerializedName("transport_code") val transportCode: String, - @SerializedName("contractor_route") val contractorRoute: String, - @SerializedName("truck_plate") val truckPlate: String, - @SerializedName("trailer_plate") val trailerPlate: String? = null, + val truckPlate: String? = null, + val trailerPlate: String? = null, val weight: Double? = null, val status: String? = null, - @SerializedName("is_external_rental") val isExternalRental: Boolean = false, - @SerializedName("external_driver_name") val externalDriverName: String? = null, + val isExternalRental: Boolean = false, + val externalDriverName: String? = null, ) /** diff --git a/app/src/main/java/pl/firmatpp/itstransport/data/repository/SchedulerRepository.kt b/app/src/main/java/pl/firmatpp/itstransport/data/repository/SchedulerRepository.kt index ef0633d..5ec6a16 100644 --- a/app/src/main/java/pl/firmatpp/itstransport/data/repository/SchedulerRepository.kt +++ b/app/src/main/java/pl/firmatpp/itstransport/data/repository/SchedulerRepository.kt @@ -49,9 +49,9 @@ class SchedulerRepository @Inject constructor( RouteDisplayItem( id = event.id, driverName = driverName, - transportCode = event.extendedProps.transportCode, - contractorRoute = event.extendedProps.contractorRoute, - truckPlate = event.extendedProps.truckPlate, + transportCode = event.title, + contractorRoute = event.description ?: "", + truckPlate = event.extendedProps.truckPlate ?: "", trailerPlate = event.extendedProps.trailerPlate, weight = event.extendedProps.weight, status = event.extendedProps.status, diff --git a/app/src/main/java/pl/firmatpp/itstransport/ui/service/wizard/ServiceSelectStep.kt b/app/src/main/java/pl/firmatpp/itstransport/ui/service/wizard/ServiceSelectStep.kt index 1cfb476..4b2e1a4 100644 --- a/app/src/main/java/pl/firmatpp/itstransport/ui/service/wizard/ServiceSelectStep.kt +++ b/app/src/main/java/pl/firmatpp/itstransport/ui/service/wizard/ServiceSelectStep.kt @@ -2,6 +2,7 @@ package pl.firmatpp.itstransport.ui.service.wizard import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -110,10 +111,12 @@ fun ServiceSelectStep( category = category, serviceTypes = types, selectedIds = selectedIds, + selectedServices = selectedServices, isExpanded = category?.id == expandedCategoryId, vehicleTab = vehicleTab, onToggleExpand = { category?.id?.let(onToggleCategory) }, onToggleService = onToggleService, + onUpdatePositions = onUpdatePositions, ) } @@ -133,8 +136,6 @@ fun ServiceSelectStep( ) { selected -> SelectedServiceDetail( selected = selected, - vehicleTab = vehicleTab, - onUpdatePositions = onUpdatePositions, onUpdateNotes = onUpdateNotes, onUpdateAmount = onUpdateAmount, ) @@ -163,20 +164,23 @@ private fun CategoryCard( category: ServiceCategory?, serviceTypes: List, selectedIds: Set, + selectedServices: List, isExpanded: Boolean, vehicleTab: AddServiceViewModel.VehicleTab, onToggleExpand: () -> Unit, onToggleService: (ServiceType) -> Unit, + onUpdatePositions: (serviceTypeId: Long, positions: List) -> Unit, ) { val categoryName = category?.name ?: "Inne" val firstLetter = categoryName.first().uppercase() + val selectedCount = serviceTypes.count { it.id in selectedIds } ElevatedCard( onClick = onToggleExpand, modifier = Modifier.fillMaxWidth(), ) { Column(modifier = Modifier.padding(12.dp)) { - // Header row: badge + name + chevron + // Header row: badge + name + count + chevron Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), @@ -206,26 +210,67 @@ private fun CategoryCard( overflow = TextOverflow.Ellipsis, ) + if (selectedCount > 0) { + Text( + text = "$selectedCount", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .padding(end = 8.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.small, + ) + .padding(horizontal = 8.dp, vertical = 2.dp), + ) + } + Icon( imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, contentDescription = if (isExpanded) "Zwiń" else "Rozwiń", ) } - // Expanded content: service type tiles + // Expanded content: service type tiles + inline position pickers AnimatedVisibility(visible = isExpanded) { - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { + Column { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + serviceTypes.forEach { serviceType -> + ServiceTypeTile( + serviceType = serviceType, + isSelected = serviceType.id in selectedIds, + onClick = { onToggleService(serviceType) }, + ) + } + } + + // Inline position pickers for selected services with position groups serviceTypes.forEach { serviceType -> - ServiceTypeTile( - serviceType = serviceType, - isSelected = serviceType.id in selectedIds, - onClick = { onToggleService(serviceType) }, + if (serviceType.id !in selectedIds) return@forEach + val positionGroups = serviceType.positionGroups + if (positionGroups.isNullOrEmpty()) return@forEach + + val filteredGroups = filterPositionGroups(positionGroups, vehicleTab) + if (filteredGroups.isEmpty()) return@forEach + + val selected = selectedServices.find { it.serviceType.id == serviceType.id } + ?: return@forEach + + Spacer(modifier = Modifier.height(10.dp)) + + InlinePositionPicker( + serviceName = serviceType.name ?: "—", + positionGroups = filteredGroups, + selectedPositions = selected.selectedPositions, + onPositionsChanged = { positions -> + onUpdatePositions(serviceType.id, positions) + }, ) } } @@ -278,15 +323,13 @@ private fun ServiceTypeTile( } // ────────────────────────────────────────────── -// SelectedServiceDetail — per-service notes, amount, positions +// SelectedServiceDetail — per-service notes and amount // ────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable private fun SelectedServiceDetail( selected: SelectedService, - vehicleTab: AddServiceViewModel.VehicleTab, - onUpdatePositions: (serviceTypeId: Long, positions: List) -> Unit, onUpdateNotes: (serviceTypeId: Long, notes: String?) -> Unit, onUpdateAmount: (serviceTypeId: Long, amount: String?) -> Unit, ) { @@ -322,22 +365,6 @@ private fun SelectedServiceDetail( AnimatedVisibility(visible = expanded.value) { Column { - // Position pickers (if service type has position groups) - val positionGroups = selected.serviceType.positionGroups - if (!positionGroups.isNullOrEmpty()) { - val filteredGroups = filterPositionGroups(positionGroups, vehicleTab) - if (filteredGroups.isNotEmpty()) { - PositionPicker( - positionGroups = filteredGroups, - selectedPositions = selected.selectedPositions, - onPositionsChanged = { positions -> - onUpdatePositions(serviceTypeId, positions) - }, - ) - Spacer(modifier = Modifier.height(8.dp)) - } - } - // Notes field OutlinedTextField( value = selected.notes ?: "", @@ -370,59 +397,69 @@ private fun SelectedServiceDetail( } // ────────────────────────────────────────────── -// PositionPicker — FilterChips grouped by PositionGroup +// InlinePositionPicker — shown inside category card for selected services +// Matches the Next.js external service pattern // ────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable -private fun PositionPicker( +private fun InlinePositionPicker( + serviceName: String, positionGroups: List, selectedPositions: List, onPositionsChanged: (List) -> Unit, ) { val selectedIds = selectedPositions.map { it.id }.toSet() - Column { - Text( - text = "Pozycje", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(4.dp)) - - positionGroups.forEach { group -> - if (group.positions.isNullOrEmpty()) return@forEach - + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + ), + ) { + Column(modifier = Modifier.padding(10.dp)) { Text( - text = group.name ?: "—", - style = MaterialTheme.typography.labelSmall, + text = "$serviceName — wybierz pozycje:", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp, bottom = 2.dp), ) + Spacer(modifier = Modifier.height(6.dp)) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - group.positions.forEach { position -> - val isSelected = position.id in selectedIds - FilterChip( - selected = isSelected, - onClick = { - val newPositions = if (isSelected) { - selectedPositions.filter { it.id != position.id } - } else { - selectedPositions + position - } - onPositionsChanged(newPositions) - }, - label = { - Text( - text = position.name ?: "—", - style = MaterialTheme.typography.bodySmall, - ) - }, - ) + positionGroups.forEach { group -> + if (group.positions.isNullOrEmpty()) return@forEach + + Text( + text = group.name ?: "—", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp, bottom = 2.dp), + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + group.positions.forEach { position -> + val isSelected = position.id in selectedIds + FilterChip( + selected = isSelected, + onClick = { + val newPositions = if (isSelected) { + selectedPositions.filter { it.id != position.id } + } else { + selectedPositions + position + } + onPositionsChanged(newPositions) + }, + label = { + Text( + text = position.name ?: "—", + style = MaterialTheme.typography.bodySmall, + ) + }, + ) + } } } }