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:
@@ -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,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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<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,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<PositionGroup>,
|
||||
selectedPositions: List<Position>,
|
||||
onPositionsChanged: (List<Position>) -> 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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user