// ─── 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 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 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 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 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 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? ?? []) .map((s) => OrderItemSubstitute.fromJson(s as Map)) .toList(), ); } class OrderSegment { final int id; final String name; final String zone; final List 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 j) => OrderSegment( id: j['id'] as int, name: j['name'] as String, zone: j['zone'] as String, items: (j['items'] as List) .map((i) => OrderItem.fromJson(i as Map)) .toList(), ); } class Order { final int id; final String orderNumber; final double amount; final DateTime slotTime; OrderStatus status; final Customer customer; final List segments; final List 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 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), segments: (j['segments'] as List) .map((s) => OrderSegment.fromJson(s as Map)) .toList(), assignedSlots: (j['assignedSlots'] as List? ?? []) .map((s) => StorageSlot.fromJson(s as Map)) .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); }