From 96387c08f44ef8ea05efdd6d0e5465846044e4fb Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 21 Nov 2025 19:53:02 +0700 Subject: [PATCH] fix split bill --- android/app/build.gradle | 2 +- lib/core/function/app_function.dart | 50 + lib/data/dataoutputs/print_dataoutputs.dart | 263 +++++ .../models/response/order_response_model.dart | 100 ++ .../payment/pages/payment_page.dart | 53 +- .../pages/success_split_bill_page.dart | 993 ++++++++++++++++++ pubspec.lock | 67 +- 7 files changed, 1471 insertions(+), 57 deletions(-) create mode 100644 lib/presentation/success/pages/success_split_bill_page.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 60d083e..6618029 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.appscale.pos" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/core/function/app_function.dart b/lib/core/function/app_function.dart index d6c5bf7..d6d3945 100644 --- a/lib/core/function/app_function.dart +++ b/lib/core/function/app_function.dart @@ -481,6 +481,56 @@ Future onPrinVoidRecipt( } } +Future onPrintSplit( + context, { + required Order order, +}) async { + final receiptPrinter = + await PrinterLocalDatasource.instance.getPrinterByCode('receipt'); + final authData = await AuthLocalDataSource().getAuthData(); + final settings = await SettingsLocalDatasource().getTax(); + final outlet = await OutletLocalDatasource().get(); + + if (receiptPrinter != null) { + try { + final printValue = await PrintDataoutputs.instance.printSplitBill( + order, + authData.user?.name ?? "", + receiptPrinter.paper.toIntegerFromText, + outlet, + ); + await PrinterService() + .printWithPrinter(receiptPrinter, printValue, context); + } catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError( + e, + stackTrace, + reason: 'Print receipt failed', + information: [ + 'Order ID: ${order.id}', + 'Printer: ${receiptPrinter.name}', + ], + ); + log("Error printing receipt order: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error printing receipt order: $e')), + ); + } + } else { + FirebaseCrashlytics.instance.recordError( + 'Kitchen printer not found', + null, + reason: 'Kitchen printer not found / Printer not setting in printer page', + information: [ + 'Order ID: ${order.id}', + ], + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Anda belum menghubungkan printer kitchen')), + ); + } +} + Future generateBarcodeAsUint8List(String data) async { // 1. Buat barcode instance (code128, qrCode, dll) final barcode = Barcode.code128(); diff --git a/lib/data/dataoutputs/print_dataoutputs.dart b/lib/data/dataoutputs/print_dataoutputs.dart index 441d36b..7e2cf8c 100644 --- a/lib/data/dataoutputs/print_dataoutputs.dart +++ b/lib/data/dataoutputs/print_dataoutputs.dart @@ -1843,4 +1843,267 @@ class PrintDataoutputs { return allBytes; } + + Future> printSplitBill( + Order order, + String chashierName, + int paper, + Outlet outlet, + ) async { + List bytes = []; + + final profile = await CapabilityProfile.load(); + final generator = + Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile); + + bytes += generator.reset(); + + bytes += generator.text(outlet.name ?? "", + styles: const PosStyles( + bold: true, + align: PosAlign.center, + height: PosTextSize.size1, + width: PosTextSize.size1, + )); + + bytes += generator.text(outlet.address ?? "", + styles: const PosStyles(bold: false, align: PosAlign.center)); + bytes += generator.text(outlet.phoneNumber ?? "", + styles: const PosStyles(bold: false, align: PosAlign.center)); + + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + + bytes += generator.row([ + PosColumn( + text: DateFormat('dd MMM yyyy').format(DateTime.now()), + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: DateFormat('HH:mm').format(DateTime.now()), + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + bytes += generator.row([ + PosColumn( + text: 'Receipt Number', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: DateFormat('yyyyMMddhhmm').format(DateTime.now()), + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + + bytes += generator.row([ + PosColumn( + text: 'Order ID', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: Random().nextInt(100000).toString(), + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + bytes += generator.row([ + PosColumn( + text: 'Bill Name', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: order.metadata?['customer_name'] ?? '', + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + bytes += generator.row([ + PosColumn( + text: 'Collected By', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: chashierName, + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + if (order.payments != null) { + bytes += generator.row([ + PosColumn( + text: 'Payment', + width: 8, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: order.payments?.last.paymentMethodName ?? '-', + width: 4, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + } + + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + bytes += generator.text(order.orderType ?? '-', + styles: const PosStyles(bold: true, align: PosAlign.center)); + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + bytes += generator.text("SPLIT", + styles: const PosStyles(bold: true, align: PosAlign.center)); + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + + for (final item in (order.orderItems ?? [])) { + bytes += generator.row([ + PosColumn( + text: '${item.quantity} x ${item.productName}', + width: 8, + styles: const PosStyles(bold: true, align: PosAlign.left), + ), + PosColumn( + text: (((item.unitPrice ?? 0) * (item.quantity ?? 0))) + .currencyFormatRpV2, + width: 4, + styles: const PosStyles(bold: true, align: PosAlign.right), + ), + ]); + if (item.notes != '') { + bytes += generator.row([ + PosColumn( + text: 'Note', + width: 4, + styles: const PosStyles(bold: false, align: PosAlign.left), + ), + PosColumn( + text: item.notes ?? "-", + width: 8, + styles: const PosStyles(bold: false, align: PosAlign.right), + ), + ]); + } + } + + if (order.orderItems?.isNotEmpty ?? false) { + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + } + + bytes += generator.row([ + PosColumn( + text: 'Subtotal', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: order.payments?.last.amount?.currencyFormatRpV2 ?? '-', + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + + bytes += generator.row([ + PosColumn( + text: 'Discount', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: (order.discountAmount ?? 0).currencyFormatRpV2, + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + + // Only show tax if it's greater than 0 + if ((order.taxAmount ?? 0) > 0) { + bytes += generator.row([ + PosColumn( + text: 'Tax PB1 (${order.taxAmount}%)', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: (order.taxAmount ?? 0).currencyFormatRpV2, + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + } + + // Only show service charge if it's greater than 0 + // if (serviceCharge > 0) { + // bytes += generator.row([ + // PosColumn( + // text: 'Service Charge($serviceChargePercentage%)', + // width: 6, + // styles: const PosStyles(align: PosAlign.left), + // ), + // PosColumn( + // text: serviceCharge.currencyFormatRpV2, + // width: 6, + // styles: const PosStyles(align: PosAlign.right), + // ), + // ]); + // } + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + bytes += generator.row([ + PosColumn( + text: 'Total', + width: 6, + styles: const PosStyles(bold: true, align: PosAlign.left), + ), + PosColumn( + text: order.payments?.last.amount?.currencyFormatRpV2 ?? '-', + width: 6, + styles: const PosStyles(bold: true, align: PosAlign.right), + ), + ]); + bytes += generator.row([ + PosColumn( + text: 'Dibayar', + width: 6, + styles: const PosStyles(align: PosAlign.left), + ), + PosColumn( + text: order.payments?.last.amount?.currencyFormatRpV2 ?? '-', + width: 6, + styles: const PosStyles(align: PosAlign.right), + ), + ]); + bytes += generator.text( + paper == 80 + ? '------------------------------------------------' + : '--------------------------------', + styles: const PosStyles(bold: false, align: PosAlign.center)); + paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1); + bytes += generator.cut(); + return bytes; + } } diff --git a/lib/data/models/response/order_response_model.dart b/lib/data/models/response/order_response_model.dart index 1a1b367..52bfd60 100644 --- a/lib/data/models/response/order_response_model.dart +++ b/lib/data/models/response/order_response_model.dart @@ -401,3 +401,103 @@ extension OrderItemListExtension on List { quantity: e.quantity ?? 0, )).toList(); } + +extension OrderCopyWith on Order { + Order copyWith({ + String? id, + String? orderNumber, + String? outletId, + String? userId, + String? tableNumber, + String? orderType, + String? status, + int? subtotal, + int? taxAmount, + int? discountAmount, + int? totalAmount, + num? totalCost, + int? remainingAmount, + String? paymentStatus, + int? refundAmount, + bool? isVoid, + bool? isRefund, + String? notes, + Map? metadata, + DateTime? createdAt, + DateTime? updatedAt, + List? orderItems, + List? payments, + int? totalPaid, + int? paymentCount, + String? splitType, + }) { + return Order( + id: id ?? this.id, + orderNumber: orderNumber ?? this.orderNumber, + outletId: outletId ?? this.outletId, + userId: userId ?? this.userId, + tableNumber: tableNumber ?? this.tableNumber, + orderType: orderType ?? this.orderType, + status: status ?? this.status, + subtotal: subtotal ?? this.subtotal, + taxAmount: taxAmount ?? this.taxAmount, + discountAmount: discountAmount ?? this.discountAmount, + totalAmount: totalAmount ?? this.totalAmount, + totalCost: totalCost ?? this.totalCost, + remainingAmount: remainingAmount ?? this.remainingAmount, + paymentStatus: paymentStatus ?? this.paymentStatus, + refundAmount: refundAmount ?? this.refundAmount, + isVoid: isVoid ?? this.isVoid, + isRefund: isRefund ?? this.isRefund, + notes: notes ?? this.notes, + metadata: metadata ?? this.metadata, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + orderItems: orderItems ?? this.orderItems, + payments: payments ?? this.payments, + totalPaid: totalPaid ?? this.totalPaid, + paymentCount: paymentCount ?? this.paymentCount, + splitType: splitType ?? this.splitType, + ); + } +} + +extension OrderItemCopyWith on OrderItem { + OrderItem copyWith({ + String? id, + String? orderId, + String? productId, + String? productName, + String? productVariantId, + String? productVariantName, + int? quantity, + int? unitPrice, + int? totalPrice, + List? modifiers, + String? notes, + String? status, + DateTime? createdAt, + DateTime? updatedAt, + String? printerType, + int? paidQuantity, + }) { + return OrderItem( + id: id ?? this.id, + orderId: orderId ?? this.orderId, + productId: productId ?? this.productId, + productName: productName ?? this.productName, + productVariantId: productVariantId ?? this.productVariantId, + productVariantName: productVariantName ?? this.productVariantName, + quantity: quantity ?? this.quantity, + unitPrice: unitPrice ?? this.unitPrice, + totalPrice: totalPrice ?? this.totalPrice, + modifiers: modifiers ?? this.modifiers, + notes: notes ?? this.notes, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + printerType: printerType ?? this.printerType, + paidQuantity: paidQuantity ?? this.paidQuantity, + ); + } +} diff --git a/lib/presentation/payment/pages/payment_page.dart b/lib/presentation/payment/pages/payment_page.dart index a66445b..c1c4880 100644 --- a/lib/presentation/payment/pages/payment_page.dart +++ b/lib/presentation/payment/pages/payment_page.dart @@ -12,6 +12,7 @@ import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_method import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; import 'package:enaklo_pos/presentation/success/pages/success_payment_page.dart'; +import 'package:enaklo_pos/presentation/success/pages/success_split_bill_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -370,23 +371,43 @@ class _PaymentPageState extends State { state.maybeWhen( orElse: () {}, success: (data) { - context.pushReplacement(SuccessPaymentPage( - productQuantity: getOrderItemPending() - .map( - (item) => ProductQuantity( - product: Product( - name: item.productName, - price: item.unitPrice, - ), - quantity: item.quantity ?? 0, + if (widget.isSplit) { + context.pushReplacement(SuccessSplitBillPage( + productQuantity: getOrderItemPending() + .map( + (item) => ProductQuantity( + product: Product( + name: item.productName, + price: item.unitPrice, ), - ) - .toList() ?? - [], - payment: data, - paymentMethod: selectedPaymentMethod?.name ?? '', - nominalBayar: totalPriceController.text.toIntegerFromText, - )); + quantity: item.quantity ?? 0, + ), + ) + .toList(), + payment: data, + paymentMethod: selectedPaymentMethod?.name ?? '', + nominalBayar: + totalPriceController.text.toIntegerFromText, + )); + } else { + context.pushReplacement(SuccessPaymentPage( + productQuantity: getOrderItemPending() + .map( + (item) => ProductQuantity( + product: Product( + name: item.productName, + price: item.unitPrice, + ), + quantity: item.quantity ?? 0, + ), + ) + .toList(), + payment: data, + paymentMethod: selectedPaymentMethod?.name ?? '', + nominalBayar: + totalPriceController.text.toIntegerFromText, + )); + } }, error: (message) { AppFlushbar.showError(context, message); diff --git a/lib/presentation/success/pages/success_split_bill_page.dart b/lib/presentation/success/pages/success_split_bill_page.dart new file mode 100644 index 0000000..f1efc97 --- /dev/null +++ b/lib/presentation/success/pages/success_split_bill_page.dart @@ -0,0 +1,993 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/core/extensions/int_ext.dart'; +import 'package:enaklo_pos/core/extensions/string_ext.dart'; +import 'package:enaklo_pos/core/function/app_function.dart'; +import 'package:enaklo_pos/data/models/response/order_response_model.dart'; +import 'package:enaklo_pos/data/models/response/payment_response_model.dart'; +import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SuccessSplitBillPage extends StatefulWidget { + final List productQuantity; + final PaymentData payment; + final String paymentMethod; + final int nominalBayar; + const SuccessSplitBillPage({ + super.key, + required this.payment, + required this.productQuantity, + required this.paymentMethod, + required this.nominalBayar, + }); + + @override + State createState() => _SuccessSplitBillPageState(); +} + +class _SuccessSplitBillPageState extends State { + @override + void initState() { + super.initState(); + context + .read() + .add(OrderLoaderEvent.getById(widget.payment.orderId ?? "")); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.primary.withOpacity(0.05), + AppColors.background, + AppColors.background, + ], + ), + ), + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => SizedBox.shrink(), + loading: () => Center( + child: CircularProgressIndicator(), + ), + loadedDetail: (order) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: Row( + children: [ + // Left Panel - Success Message & Order Info + Expanded( + flex: 35, + child: _buildLeftPanel(order), + ), + + const SizedBox(width: 16), + + // Right Panel - Order Details + Expanded( + flex: 65, + child: _buildRightPanel(order), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ), + ); + } + + Widget _buildLeftPanel(Order order) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.1), + blurRadius: 30, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + // Success Header + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(32.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + child: Column( + children: [ + // Success Icon + Container( + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.check_rounded, + size: 48, + color: Colors.white, + ), + ), + + const SizedBox(height: 24), + + // Success Title + const Text( + 'Split Bill Berhasil!', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + + const SizedBox(height: 12), + + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Pesanan telah diterima dan sedang diproses', + style: TextStyle( + fontSize: 14, + color: AppColors.primary, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + // Order Information Section + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle('Informasi Pesanan'), + const SizedBox(height: 24), + + // Customer Card + _buildInfoCard( + icon: Icons.person_outline_rounded, + title: 'Nama Pelanggan', + value: order.metadata?['customer_name'] ?? "-", + gradient: [ + Colors.blue.withOpacity(0.1), + Colors.purple.withOpacity(0.1), + ], + ), + + const SizedBox(height: 16), + + // Order Details + Column( + children: [ + _buildInfoRow( + icon: Icons.receipt_long_outlined, + label: 'No. Pesanan', + value: order.orderNumber ?? "-", + ), + const SizedBox(height: 12), + _buildInfoRow( + icon: Icons.receipt_long_outlined, + label: 'Metode Pembayaran', + value: widget.paymentMethod, + ), + const SizedBox(height: 12), + _buildInfoRow( + icon: Icons.access_time_rounded, + label: 'Waktu', + value: (order.createdAt ?? DateTime.now()) + .toFormattedDate3(), + ), + const SizedBox(height: 12), + _buildInfoRow( + icon: Icons.check_circle_outline, + label: 'Status Pembayaran', + value: 'Lunas', + valueColor: Colors.green, + showBadge: true, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + + // Total and Action Buttons + _buildBottomSection(order), + ], + ), + ); + } + + Widget _buildRightPanel(Order order) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 30, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.background, + Colors.grey.shade50, + ], + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary.withOpacity(0.2), + AppColors.primary.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(16.0), + ), + child: Icon( + Icons.receipt_long_rounded, + color: AppColors.primary, + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Detail Pesanan', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Ringkasan item yang dipesan', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + '${widget.productQuantity.length} Items', + style: const TextStyle( + fontSize: 13, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + + // Product List + Expanded( + child: ListView.separated( + padding: const EdgeInsets.all(24.0), + itemCount: widget.productQuantity.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + return _buildProductCard(index); + }, + ), + ), + + // Summary Footer + _buildSummaryFooter(order), + ], + ), + ); + } + + Widget _buildProductCard(int index) { + final item = widget.productQuantity[index]; + final totalPrice = (item.product.price ?? 0) * item.quantity; + + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey.shade50, + Colors.white, + ], + ), + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: Colors.grey.withOpacity(0.1), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Product Image + Container( + width: 70, + height: 70, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary.withOpacity(0.2), + AppColors.primary.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(16.0), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + Icons.restaurant_rounded, + color: AppColors.primary, + size: 28, + ), + ), + + const SizedBox(width: 16), + + // Product Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.product.name ?? "-", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + (item.product.price ?? 0).toString().currencyFormatRpV2, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + const SizedBox(width: 16), + + // Quantity and Total + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + '${item.quantity}x', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + totalPrice.toString().currencyFormatRpV2, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildInfoCard({ + required IconData icon, + required String title, + required String value, + required List gradient, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradient, + ), + borderRadius: BorderRadius.circular(16.0), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + size: 20, + color: AppColors.primary, + ), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + ); + } + + Widget _buildInfoRow({ + required IconData icon, + required String label, + required String value, + Color? valueColor, + bool showBadge = false, + }) { + return Row( + children: [ + Icon( + icon, + size: 18, + color: Colors.grey.shade600, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ), + if (showBadge && valueColor != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: valueColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + size: 14, + color: valueColor, + ), + const SizedBox(width: 4), + Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: valueColor, + ), + ), + ], + ), + ) + else + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: valueColor ?? Colors.black87, + ), + ), + ], + ); + } + + Widget _buildBottomSection(Order order) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.grey.shade50, + Colors.white, + ], + ), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(24), + ), + ), + child: Column( + children: [ + // Total Amount + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.primary.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total Pembayaran', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + Text( + widget.nominalBayar.currencyFormatRpV2, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Action Buttons + Row( + children: [ + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.primary.withOpacity(0.3), + width: 2, + ), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(14), + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () { + context.push(DashboardPage()); + }, + child: const Center( + child: Text( + 'Kembali ke Beranda', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + final updatedOrderItems = + widget.productQuantity.map((pq) { + return OrderItem( + productName: pq.product.name, + printerType: pq.product.printerType, + productVariantName: pq.variant?.name, + quantity: pq.quantity, + unitPrice: pq.product.price, + totalPrice: (pq.product.price ?? 0) * (pq.quantity), + ); + }).toList(); + onPrintSplit( + context, + order: order.copyWith( + orderItems: updatedOrderItems, + ), + ); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.print_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 8), + Text( + 'Cetak Struk', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSummaryFooter(Order order) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.grey.shade50, + Colors.white, + ], + ), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(24), + ), + ), + child: Column( + children: [ + // Decorative Divider + Container( + height: 1, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + AppColors.primary.withOpacity(0.3), + Colors.transparent, + ], + ), + ), + ), + + // Subtotal Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.shopping_cart_outlined, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 8), + Text( + 'Subtotal (${widget.productQuantity.length} items)', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + Text( + widget.nominalBayar.currencyFormatRpV2, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Total Payment Row + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColors.primary.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.payments_rounded, + size: 16, + color: AppColors.primary, + ), + ), + const SizedBox(width: 12), + const Text( + 'Total Pembayaran', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + widget.nominalBayar.currencyFormatRpV2, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 3d2aa3f..8bdeb93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -17,19 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.61" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.4.1" another_flushbar: dependency: "direct main" description: @@ -114,10 +109,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" build_config: dependency: transitive description: @@ -138,26 +133,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -322,10 +317,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.6" dartx: dependency: transitive description: @@ -633,10 +628,10 @@ packages: dependency: "direct main" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -857,26 +852,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -893,14 +888,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -1470,10 +1457,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" time: dependency: transitive description: @@ -1534,10 +1521,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -1635,5 +1622,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.0"