import 'package:equatable/equatable.dart'; import '../../core/utils/extensions.dart'; import 'transaction.dart'; /// Everything the back office needs from a day at this terminal. /// /// Computed from the locally stored transactions, so it can be produced with /// no connection and pushed whenever one is available. class ShiftReport extends Equatable { const ShiftReport({ required this.businessDate, required this.terminalId, required this.cashierName, required this.billCount, required this.itemCount, required this.grossSales, required this.taxCollected, required this.discountGiven, required this.roundOff, required this.paymentBreakdown, required this.loyaltyPointsIssued, required this.loyaltyPointsRedeemed, this.firstBillAt, this.lastBillAt, }); final DateTime businessDate; final String terminalId; final String cashierName; final int billCount; /// Total units sold across every bill. final double itemCount; final double grossSales; final double taxCollected; final double discountGiven; final double roundOff; final Map paymentBreakdown; final int loyaltyPointsIssued; final int loyaltyPointsRedeemed; final DateTime? firstBillAt; final DateTime? lastBillAt; /// A zeroed report for a day with no trading. Not a const — [DateTime] /// cannot appear in a constant expression. factory ShiftReport.blank({ required DateTime businessDate, required String terminalId, required String cashierName, }) => ShiftReport( businessDate: businessDate, terminalId: terminalId, cashierName: cashierName, billCount: 0, itemCount: 0, grossSales: 0, taxCollected: 0, discountGiven: 0, roundOff: 0, paymentBreakdown: const {}, loyaltyPointsIssued: 0, loyaltyPointsRedeemed: 0, ); bool get isEmpty => billCount == 0; double get averageBasket => billCount == 0 ? 0 : (grossSales / billCount).asMoney; double get netOfTax => (grossSales - taxCollected).asMoney; /// Builds the report from the day's transactions. factory ShiftReport.fromTransactions({ required List transactions, required DateTime businessDate, required String terminalId, required String cashierName, }) { final completed = transactions .where((t) => t.status == TransactionStatus.completed && t.createdAt.year == businessDate.year && t.createdAt.month == businessDate.month && t.createdAt.day == businessDate.day) .toList() ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); final byMethod = {}; for (final t in completed) { for (final p in t.payments) { byMethod[p.method] = ((byMethod[p.method] ?? 0) + p.amount).asMoney; } } return ShiftReport( businessDate: businessDate, terminalId: terminalId, cashierName: cashierName, billCount: completed.length, itemCount: completed.fold(0.0, (s, t) => s + t.cart.totalQuantity), grossSales: completed.fold(0.0, (s, t) => s + t.total).asMoney, taxCollected: completed.fold(0.0, (s, t) => s + t.cart.taxAmount).asMoney, discountGiven: completed .fold(0.0, (s, t) => s + t.cart.billDiscountTotal + t.cart.lineDiscountTotal) .asMoney, roundOff: completed.fold(0.0, (s, t) => s + t.cart.roundOff).asMoney, paymentBreakdown: byMethod, loyaltyPointsIssued: completed.fold(0, (s, t) => s + t.pointsEarned), loyaltyPointsRedeemed: completed.fold(0, (s, t) => s + t.pointsRedeemed), firstBillAt: completed.isEmpty ? null : completed.first.createdAt, lastBillAt: completed.isEmpty ? null : completed.last.createdAt, ); } /// The JSON body that would be sent to the back office. Map toPayload() => { 'business_date': businessDate.toIso8601String().substring(0, 10), 'terminal_id': terminalId, 'cashier': cashierName, 'bill_count': billCount, 'item_count': itemCount, 'gross_sales': grossSales, 'net_of_tax': netOfTax, 'tax_collected': taxCollected, 'discount_given': discountGiven, 'round_off': roundOff, 'average_basket': averageBasket, 'loyalty_points_issued': loyaltyPointsIssued, 'loyalty_points_redeemed': loyaltyPointsRedeemed, 'first_bill_at': firstBillAt?.toIso8601String(), 'last_bill_at': lastBillAt?.toIso8601String(), 'payments': { for (final e in paymentBreakdown.entries) e.key.name: e.value, }, }; @override List get props => [businessDate, terminalId, billCount, grossSales]; }