- All 5 batch steps: Picking, Sorting, Clarification, Slots, Handoff - go_router navigation with step indicator - graphql_flutter client wired up (endpoint via env var) - Mock data layer swappable with real GraphQL service - Item types: normal, cold, frozen, alcohol, clarify - Storage slot assignment (cell/freezer/fridge)
234 lines
7.2 KiB
Dart
234 lines
7.2 KiB
Dart
// ─── Enums ────────────────────────────────────────────────────────────────────
|
||
|
||
enum OrderStatus { newOrder, inProgress, picked, stored, issued }
|
||
|
||
enum ItemStatus { newItem, inProgress, picked, notFound, replaced }
|
||
|
||
enum ItemType { normal, cold, frozen, alcohol, clarify }
|
||
|
||
enum BatchStatus { newBatch, inProgress, completed, cancelled }
|
||
|
||
enum StorageSlotType { cell, freezer, fridge }
|
||
|
||
// ─── Models ───────────────────────────────────────────────────────────────────
|
||
|
||
class Customer {
|
||
final String name;
|
||
final String phone;
|
||
final String? comment;
|
||
|
||
const Customer({required this.name, required this.phone, this.comment});
|
||
|
||
factory Customer.fromJson(Map<String, dynamic> j) => Customer(
|
||
name: j['name'] as String,
|
||
phone: j['phone'] as String,
|
||
comment: j['comment'] as String?,
|
||
);
|
||
}
|
||
|
||
class StorageSlot {
|
||
final int id;
|
||
final StorageSlotType type;
|
||
final String name;
|
||
final int maxOrders;
|
||
|
||
const StorageSlot({required this.id, required this.type, required this.name, required this.maxOrders});
|
||
|
||
String get typeLabel {
|
||
switch (type) {
|
||
case StorageSlotType.cell:
|
||
return 'Ячейка';
|
||
case StorageSlotType.freezer:
|
||
return 'Морозилка';
|
||
case StorageSlotType.fridge:
|
||
return 'Холодильник';
|
||
}
|
||
}
|
||
|
||
factory StorageSlot.fromJson(Map<String, dynamic> j) => StorageSlot(
|
||
id: j['id'] as int,
|
||
type: StorageSlotType.values.firstWhere((e) => e.name == j['type']),
|
||
name: j['name'] as String,
|
||
maxOrders: j['maxOrders'] as int,
|
||
);
|
||
}
|
||
|
||
class OrderItemSubstitute {
|
||
final String sku;
|
||
final String name;
|
||
final int priority;
|
||
|
||
const OrderItemSubstitute({required this.sku, required this.name, required this.priority});
|
||
|
||
factory OrderItemSubstitute.fromJson(Map<String, dynamic> j) => OrderItemSubstitute(
|
||
sku: j['sku'] as String,
|
||
name: j['name'] as String,
|
||
priority: j['priority'] as int,
|
||
);
|
||
}
|
||
|
||
class OrderItem {
|
||
final int id;
|
||
final String sku;
|
||
final String name;
|
||
final String? imageUrl;
|
||
final String category;
|
||
final double price;
|
||
final double planQty;
|
||
double actualQty;
|
||
ItemStatus status;
|
||
ItemType type;
|
||
final List<OrderItemSubstitute> substitutes;
|
||
StorageSlot? assignedSlot;
|
||
|
||
OrderItem({
|
||
required this.id,
|
||
required this.sku,
|
||
required this.name,
|
||
this.imageUrl,
|
||
required this.category,
|
||
required this.price,
|
||
required this.planQty,
|
||
this.actualQty = 0,
|
||
this.status = ItemStatus.newItem,
|
||
this.type = ItemType.normal,
|
||
this.substitutes = const [],
|
||
this.assignedSlot,
|
||
});
|
||
|
||
bool get isCompleted => status == ItemStatus.picked || status == ItemStatus.replaced || status == ItemStatus.notFound;
|
||
|
||
factory OrderItem.fromJson(Map<String, dynamic> j) => OrderItem(
|
||
id: j['id'] as int,
|
||
sku: j['sku'] as String,
|
||
name: j['name'] as String,
|
||
imageUrl: j['imageUrl'] as String?,
|
||
category: j['category'] as String,
|
||
price: (j['price'] as num).toDouble(),
|
||
planQty: (j['planQty'] as num).toDouble(),
|
||
actualQty: (j['actualQty'] as num?)?.toDouble() ?? 0,
|
||
status: ItemStatus.values.firstWhere(
|
||
(e) => e.name == (j['status'] ?? 'newItem'),
|
||
orElse: () => ItemStatus.newItem,
|
||
),
|
||
type: ItemType.values.firstWhere(
|
||
(e) => e.name == (j['type'] ?? 'normal'),
|
||
orElse: () => ItemType.normal,
|
||
),
|
||
substitutes: (j['substitutes'] as List<dynamic>? ?? [])
|
||
.map((s) => OrderItemSubstitute.fromJson(s as Map<String, dynamic>))
|
||
.toList(),
|
||
);
|
||
}
|
||
|
||
class OrderSegment {
|
||
final int id;
|
||
final String name;
|
||
final String zone;
|
||
final List<OrderItem> items;
|
||
|
||
const OrderSegment({
|
||
required this.id,
|
||
required this.name,
|
||
required this.zone,
|
||
required this.items,
|
||
});
|
||
|
||
int get pickedCount => items.where((i) => i.isCompleted).length;
|
||
int get totalCount => items.length;
|
||
bool get isCompleted => pickedCount == totalCount;
|
||
double get totalWeight => items.fold(0, (sum, i) => sum + i.planQty);
|
||
|
||
factory OrderSegment.fromJson(Map<String, dynamic> j) => OrderSegment(
|
||
id: j['id'] as int,
|
||
name: j['name'] as String,
|
||
zone: j['zone'] as String,
|
||
items: (j['items'] as List<dynamic>)
|
||
.map((i) => OrderItem.fromJson(i as Map<String, dynamic>))
|
||
.toList(),
|
||
);
|
||
}
|
||
|
||
class Order {
|
||
final int id;
|
||
final String orderNumber;
|
||
final double amount;
|
||
final DateTime slotTime;
|
||
OrderStatus status;
|
||
final Customer customer;
|
||
final List<OrderSegment> segments;
|
||
final List<StorageSlot> assignedSlots;
|
||
|
||
Order({
|
||
required this.id,
|
||
required this.orderNumber,
|
||
required this.amount,
|
||
required this.slotTime,
|
||
this.status = OrderStatus.newOrder,
|
||
required this.customer,
|
||
required this.segments,
|
||
this.assignedSlots = const [],
|
||
});
|
||
|
||
int get totalItems => segments.fold(0, (sum, s) => sum + s.totalCount);
|
||
int get pickedItems => segments.fold(0, (sum, s) => sum + s.pickedCount);
|
||
bool get isFullyPicked => pickedItems == totalItems;
|
||
double get progress => totalItems == 0 ? 0 : pickedItems / totalItems;
|
||
|
||
String get statusLabel {
|
||
switch (status) {
|
||
case OrderStatus.newOrder:
|
||
return 'Новий';
|
||
case OrderStatus.inProgress:
|
||
return 'В сборці';
|
||
case OrderStatus.picked:
|
||
return 'Зібрано';
|
||
case OrderStatus.stored:
|
||
return 'В зберіганні';
|
||
case OrderStatus.issued:
|
||
return 'Видано';
|
||
}
|
||
}
|
||
|
||
factory Order.fromJson(Map<String, dynamic> j) => Order(
|
||
id: j['id'] as int,
|
||
orderNumber: j['orderNumber'] as String,
|
||
amount: (j['amount'] as num).toDouble(),
|
||
slotTime: DateTime.parse(j['slotTime'] as String),
|
||
status: OrderStatus.values.firstWhere(
|
||
(e) => e.name == (j['status'] ?? 'newOrder'),
|
||
orElse: () => OrderStatus.newOrder,
|
||
),
|
||
customer: Customer.fromJson(j['customer'] as Map<String, dynamic>),
|
||
segments: (j['segments'] as List<dynamic>)
|
||
.map((s) => OrderSegment.fromJson(s as Map<String, dynamic>))
|
||
.toList(),
|
||
assignedSlots: (j['assignedSlots'] as List<dynamic>? ?? [])
|
||
.map((s) => StorageSlot.fromJson(s as Map<String, dynamic>))
|
||
.toList(),
|
||
);
|
||
}
|
||
|
||
// ─── Batch Step ───────────────────────────────────────────────────────────────
|
||
|
||
enum BatchStep { picking, sorting, clarification, slots, handoff }
|
||
|
||
extension BatchStepX on BatchStep {
|
||
String get label {
|
||
switch (this) {
|
||
case BatchStep.picking:
|
||
return 'Збір';
|
||
case BatchStep.sorting:
|
||
return 'Сортування';
|
||
case BatchStep.clarification:
|
||
return 'Уточнення';
|
||
case BatchStep.slots:
|
||
return 'Ячейки';
|
||
case BatchStep.handoff:
|
||
return 'Видача';
|
||
}
|
||
}
|
||
|
||
int get index => BatchStep.values.indexOf(this);
|
||
}
|