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,
@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,
)
/**

View File

@@ -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,

View File

@@ -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<ServiceType>,
selectedIds: Set<Long>,
selectedServices: List<SelectedService>,
isExpanded: Boolean,
vehicleTab: AddServiceViewModel.VehicleTab,
onToggleExpand: () -> Unit,
onToggleService: (ServiceType) -> Unit,
onUpdatePositions: (serviceTypeId: Long, positions: List<Position>) -> 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,14 +210,30 @@ 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) {
Column {
FlowRow(
modifier = Modifier
.fillMaxWidth()
@@ -229,6 +249,31 @@ private fun CategoryCard(
)
}
}
// Inline position pickers for selected services with position groups
serviceTypes.forEach { 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<Position>) -> 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,25 +397,34 @@ 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<PositionGroup>,
selectedPositions: List<Position>,
onPositionsChanged: (List<Position>) -> Unit,
) {
val selectedIds = selectedPositions.map { it.id }.toSet()
Column {
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
),
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "Pozycje",
text = "$serviceName — wybierz pozycje:",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
positionGroups.forEach { group ->
if (group.positions.isNullOrEmpty()) return@forEach
@@ -428,6 +464,7 @@ private fun PositionPicker(
}
}
}
}
// ──────────────────────────────────────────────
// NavigationButtons — Wstecz / Dalej