import 'dart:async'; import 'package:uuid/uuid.dart'; import '../models/shipment_models.dart'; import 'business_calendar_mapper.dart'; import 'sales_order_repository.dart'; import 'shipment_repository.dart'; class ShipmentLineInput { const ShipmentLineInput({ required this.description, required this.quantity, this.orderItemId, }); final String description; final int quantity; final String? orderItemId; } class ShipmentService { ShipmentService({ ShipmentRepository? shipmentRepository, SalesOrderRepository? orderRepository, BusinessCalendarMapper? calendarMapper, }) : _shipmentRepository = shipmentRepository ?? ShipmentRepository(), _orderRepository = orderRepository ?? SalesOrderRepository(), _calendarMapper = calendarMapper ?? BusinessCalendarMapper(); final ShipmentRepository _shipmentRepository; final SalesOrderRepository _orderRepository; final BusinessCalendarMapper _calendarMapper; final Uuid _uuid = const Uuid(); static const Map> _transitions = { ShipmentStatus.pending: [ShipmentStatus.picking, ShipmentStatus.cancelled], ShipmentStatus.picking: [ShipmentStatus.ready, ShipmentStatus.cancelled], ShipmentStatus.ready: [ShipmentStatus.shipped, ShipmentStatus.cancelled], ShipmentStatus.shipped: [ShipmentStatus.delivered], ShipmentStatus.delivered: [], ShipmentStatus.cancelled: [], }; Future> fetchShipments({ShipmentStatus? status}) { return _shipmentRepository.fetchShipments(status: status); } Future createShipment({ String? orderId, String? orderNumberSnapshot, String? customerNameSnapshot, List lines = const [], DateTime? scheduledShipDate, DateTime? actualShipDate, String? carrierName, String? trackingNumber, String? trackingUrl, String? labelPdfPath, String? notes, }) async { String? resolvedOrderId = orderId; String? resolvedOrderNumber = orderNumberSnapshot; String? resolvedCustomerName = customerNameSnapshot; if (resolvedOrderId != null && (resolvedOrderNumber == null || resolvedCustomerName == null)) { final order = await _orderRepository.findById(resolvedOrderId); if (order != null) { resolvedOrderNumber = order.orderNumber ?? order.id.substring(0, 6); resolvedCustomerName = order.customerNameSnapshot; } } resolvedCustomerName ??= '未設定'; final now = DateTime.now(); final shipmentId = _uuid.v4(); final shipment = Shipment( id: shipmentId, orderId: resolvedOrderId, orderNumberSnapshot: resolvedOrderNumber, customerNameSnapshot: resolvedCustomerName, scheduledShipDate: scheduledShipDate, actualShipDate: actualShipDate, status: ShipmentStatus.pending, carrierName: carrierName, trackingNumber: trackingNumber, trackingUrl: trackingUrl, labelPdfPath: labelPdfPath, notes: notes, pickingCompletedAt: null, createdAt: now, updatedAt: now, items: _buildItems(shipmentId, lines), ); await _shipmentRepository.upsertShipment(shipment); unawaited(_calendarMapper.syncShipment(shipment)); return shipment; } Future updateShipment( Shipment shipment, { List? replacedLines, DateTime? scheduledShipDate, DateTime? actualShipDate, String? carrierName, String? trackingNumber, String? trackingUrl, String? labelPdfPath, String? notes, }) async { final updated = shipment.copyWith( scheduledShipDate: scheduledShipDate ?? shipment.scheduledShipDate, actualShipDate: actualShipDate ?? shipment.actualShipDate, carrierName: carrierName ?? shipment.carrierName, trackingNumber: trackingNumber ?? shipment.trackingNumber, trackingUrl: trackingUrl ?? shipment.trackingUrl, labelPdfPath: labelPdfPath ?? shipment.labelPdfPath, notes: notes ?? shipment.notes, updatedAt: DateTime.now(), items: replacedLines != null ? _buildItems(shipment.id, replacedLines) : shipment.items, ); await _shipmentRepository.upsertShipment(updated); unawaited(_calendarMapper.syncShipment(updated)); return updated; } Future transitionStatus(String shipmentId, ShipmentStatus nextStatus, {bool force = false}) async { final shipment = await _shipmentRepository.findById(shipmentId); if (shipment == null) { throw StateError('shipment not found: $shipmentId'); } if (!force && !_canTransition(shipment.status, nextStatus)) { throw StateError('invalid transition ${shipment.status.name} -> ${nextStatus.name}'); } final now = DateTime.now(); final updated = shipment.copyWith( status: nextStatus, updatedAt: now, actualShipDate: nextStatus == ShipmentStatus.shipped || nextStatus == ShipmentStatus.delivered ? (shipment.actualShipDate ?? now) : shipment.actualShipDate, pickingCompletedAt: nextStatus == ShipmentStatus.ready ? (shipment.pickingCompletedAt ?? now) : shipment.pickingCompletedAt, ); await _shipmentRepository.upsertShipment(updated); unawaited(_calendarMapper.syncShipment(updated)); return updated; } Future advanceStatus(String shipmentId) async { final shipment = await _shipmentRepository.findById(shipmentId); if (shipment == null) { throw StateError('shipment not found: $shipmentId'); } final candidates = _transitions[shipment.status]; if (candidates == null || candidates.isEmpty) { return shipment; } return transitionStatus(shipmentId, candidates.first); } List nextStatuses(ShipmentStatus current) { return List.unmodifiable(_transitions[current] ?? const []); } bool _canTransition(ShipmentStatus current, ShipmentStatus next) { final allowed = _transitions[current]; return allowed?.contains(next) ?? false; } List _buildItems(String shipmentId, List lines) { return lines.asMap().entries.map((entry) { final line = entry.value; return ShipmentItem( id: _uuid.v4(), shipmentId: shipmentId, orderItemId: line.orderItemId, description: line.description, quantity: line.quantity, ); }).toList(); } }