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,
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,14 +210,30 @@ 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) {
|
||||||
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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)
|
@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,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)
|
@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(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.outlinedCardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(10.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pozycje",
|
text = "$serviceName — wybierz pozycje:",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
positionGroups.forEach { group ->
|
positionGroups.forEach { group ->
|
||||||
if (group.positions.isNullOrEmpty()) return@forEach
|
if (group.positions.isNullOrEmpty()) return@forEach
|
||||||
@@ -427,6 +463,7 @@ private fun PositionPicker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user