Fibe Agent 5ba2691eac feat: initial Velmart Picker Flutter app
- 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)
2026-04-23 21:43:07 +00:00

234 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── 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);
}