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) <noreply@anthropic.com>
This commit is contained in:
admin
2026-03-25 15:39:49 +01:00
parent 1c19456516
commit 13ed7d911e
3 changed files with 119 additions and 83 deletions

View File

@@ -11,8 +11,9 @@ data class SchedulerEventResponse(
val title: String, val title: String,
@SerializedName("resourceId") val resourceId: String, @SerializedName("resourceId") val resourceId: String,
val start: String, val start: String,
val end: String, val end: String? = null,
val color: String? = null, val color: String? = null,
val description: String? = null,
@SerializedName("extendedProps") val extendedProps: EventExtendedProps, @SerializedName("extendedProps") val extendedProps: EventExtendedProps,
) )
@@ -21,14 +22,12 @@ data class SchedulerEventResponse(
* Contains the transport-specific fields displayed on route cards. * Contains the transport-specific fields displayed on route cards.
*/ */
data class EventExtendedProps( data class EventExtendedProps(
@SerializedName("transport_code") val transportCode: String, val truckPlate: String? = null,
@SerializedName("contractor_route") val contractorRoute: String, val trailerPlate: String? = null,
@SerializedName("truck_plate") val truckPlate: String,
@SerializedName("trailer_plate") val trailerPlate: String? = null,
val weight: Double? = null, val weight: Double? = null,
val status: String? = null, val status: String? = null,
@SerializedName("is_external_rental") val isExternalRental: Boolean = false, val isExternalRental: Boolean = false,
@SerializedName("external_driver_name") val externalDriverName: String? = null, val externalDriverName: String? = null,
) )
/** /**

View File

@@ -49,9 +49,9 @@ class SchedulerRepository @Inject constructor(
RouteDisplayItem( RouteDisplayItem(
id = event.id, id = event.id,
driverName = driverName, driverName = driverName,
transportCode = event.extendedProps.transportCode, transportCode = event.title,
contractorRoute = event.extendedProps.contractorRoute, contractorRoute = event.description ?: "",
truckPlate = event.extendedProps.truckPlate, truckPlate = event.extendedProps.truckPlate ?: "",
trailerPlate = event.extendedProps.trailerPlate, trailerPlate = event.extendedProps.trailerPlate,
weight = event.extendedProps.weight, weight = event.extendedProps.weight,
status = event.extendedProps.status, status = event.extendedProps.status,

View File

@@ -2,6 +2,7 @@ package pl.firmatpp.itstransport.ui.service.wizard
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -110,10 +111,12 @@ fun ServiceSelectStep(
category = category, category = category,
serviceTypes = types, serviceTypes = types,
selectedIds = selectedIds, selectedIds = selectedIds,
selectedServices = selectedServices,
isExpanded = category?.id == expandedCategoryId, isExpanded = category?.id == expandedCategoryId,
vehicleTab = vehicleTab, vehicleTab = vehicleTab,
onToggleExpand = { category?.id?.let(onToggleCategory) }, onToggleExpand = { category?.id?.let(onToggleCategory) },
onToggleService = onToggleService, onToggleService = onToggleService,
onUpdatePositions = onUpdatePositions,
) )
} }
@@ -133,8 +136,6 @@ fun ServiceSelectStep(
) { selected -> ) { selected ->
SelectedServiceDetail( SelectedServiceDetail(
selected = selected, selected = selected,
vehicleTab = vehicleTab,
onUpdatePositions = onUpdatePositions,
onUpdateNotes = onUpdateNotes, onUpdateNotes = onUpdateNotes,
onUpdateAmount = onUpdateAmount, onUpdateAmount = onUpdateAmount,
) )
@@ -163,20 +164,23 @@ private fun CategoryCard(
category: ServiceCategory?, category: ServiceCategory?,
serviceTypes: List<ServiceType>, serviceTypes: List<ServiceType>,
selectedIds: Set<Long>, selectedIds: Set<Long>,
selectedServices: List<SelectedService>,
isExpanded: Boolean, isExpanded: Boolean,
vehicleTab: AddServiceViewModel.VehicleTab, vehicleTab: AddServiceViewModel.VehicleTab,
onToggleExpand: () -> Unit, onToggleExpand: () -> Unit,
onToggleService: (ServiceType) -> Unit, onToggleService: (ServiceType) -> Unit,
onUpdatePositions: (serviceTypeId: Long, positions: List<Position>) -> Unit,
) { ) {
val categoryName = category?.name ?: "Inne" val categoryName = category?.name ?: "Inne"
val firstLetter = categoryName.first().uppercase() val firstLetter = categoryName.first().uppercase()
val selectedCount = serviceTypes.count { it.id in selectedIds }
ElevatedCard( ElevatedCard(
onClick = onToggleExpand, onClick = onToggleExpand,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
// Header row: badge + name + chevron // Header row: badge + name + count + chevron
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -206,26 +210,67 @@ private fun CategoryCard(
overflow = TextOverflow.Ellipsis, 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( Icon(
imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
contentDescription = if (isExpanded) "Zwiń" else "Rozwiń", contentDescription = if (isExpanded) "Zwiń" else "Rozwiń",
) )
} }
// Expanded content: service type tiles // Expanded content: service type tiles + inline position pickers
AnimatedVisibility(visible = isExpanded) { AnimatedVisibility(visible = isExpanded) {
FlowRow( Column {
modifier = Modifier FlowRow(
.fillMaxWidth() modifier = Modifier
.padding(top = 8.dp), .fillMaxWidth()
horizontalArrangement = Arrangement.spacedBy(8.dp), .padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(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 -> serviceTypes.forEach { serviceType ->
ServiceTypeTile( if (serviceType.id !in selectedIds) return@forEach
serviceType = serviceType, val positionGroups = serviceType.positionGroups
isSelected = serviceType.id in selectedIds, if (positionGroups.isNullOrEmpty()) return@forEach
onClick = { onToggleService(serviceType) },
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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
private fun SelectedServiceDetail( private fun SelectedServiceDetail(
selected: SelectedService, selected: SelectedService,
vehicleTab: AddServiceViewModel.VehicleTab,
onUpdatePositions: (serviceTypeId: Long, positions: List<Position>) -> Unit,
onUpdateNotes: (serviceTypeId: Long, notes: String?) -> Unit, onUpdateNotes: (serviceTypeId: Long, notes: String?) -> Unit,
onUpdateAmount: (serviceTypeId: Long, amount: String?) -> Unit, onUpdateAmount: (serviceTypeId: Long, amount: String?) -> Unit,
) { ) {
@@ -322,22 +365,6 @@ private fun SelectedServiceDetail(
AnimatedVisibility(visible = expanded.value) { AnimatedVisibility(visible = expanded.value) {
Column { 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 // Notes field
OutlinedTextField( OutlinedTextField(
value = selected.notes ?: "", 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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
private fun PositionPicker( private fun InlinePositionPicker(
serviceName: String,
positionGroups: List<PositionGroup>, positionGroups: List<PositionGroup>,
selectedPositions: List<Position>, selectedPositions: List<Position>,
onPositionsChanged: (List<Position>) -> Unit, onPositionsChanged: (List<Position>) -> Unit,
) { ) {
val selectedIds = selectedPositions.map { it.id }.toSet() val selectedIds = selectedPositions.map { it.id }.toSet()
Column { OutlinedCard(
Text( modifier = Modifier.fillMaxWidth(),
text = "Pozycje", colors = CardDefaults.outlinedCardColors(
style = MaterialTheme.typography.labelMedium, containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
color = MaterialTheme.colorScheme.onSurfaceVariant, ),
) ) {
Spacer(modifier = Modifier.height(4.dp)) Column(modifier = Modifier.padding(10.dp)) {
positionGroups.forEach { group ->
if (group.positions.isNullOrEmpty()) return@forEach
Text( Text(
text = group.name ?: "", text = "$serviceName — wybierz pozycje:",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp, bottom = 2.dp),
) )
Spacer(modifier = Modifier.height(6.dp))
FlowRow( positionGroups.forEach { group ->
horizontalArrangement = Arrangement.spacedBy(6.dp), if (group.positions.isNullOrEmpty()) return@forEach
verticalArrangement = Arrangement.spacedBy(4.dp),
) { Text(
group.positions.forEach { position -> text = group.name ?: "",
val isSelected = position.id in selectedIds style = MaterialTheme.typography.labelSmall,
FilterChip( color = MaterialTheme.colorScheme.onSurfaceVariant,
selected = isSelected, modifier = Modifier.padding(top = 4.dp, bottom = 2.dp),
onClick = { )
val newPositions = if (isSelected) {
selectedPositions.filter { it.id != position.id } FlowRow(
} else { horizontalArrangement = Arrangement.spacedBy(6.dp),
selectedPositions + position verticalArrangement = Arrangement.spacedBy(4.dp),
} ) {
onPositionsChanged(newPositions) group.positions.forEach { position ->
}, val isSelected = position.id in selectedIds
label = { FilterChip(
Text( selected = isSelected,
text = position.name ?: "", onClick = {
style = MaterialTheme.typography.bodySmall, val newPositions = if (isSelected) {
) selectedPositions.filter { it.id != position.id }
}, } else {
) selectedPositions + position
}
onPositionsChanged(newPositions)
},
label = {
Text(
text = position.name ?: "",
style = MaterialTheme.typography.bodySmall,
)
},
)
}
} }
} }
} }