diff --git a/lib/presentation/void/pages/success_void_page.dart b/lib/presentation/void/pages/success_void_page.dart new file mode 100644 index 0000000..8e7f8dd --- /dev/null +++ b/lib/presentation/void/pages/success_void_page.dart @@ -0,0 +1,670 @@ +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:flutter/material.dart'; +import 'package:enaklo_pos/data/models/response/order_response_model.dart'; + +class SuccessVoidPage extends StatefulWidget { + final Order voidedOrder; + final String voidType; + final int voidAmount; + final List? voidedItems; + final String voidReason; + + const SuccessVoidPage({ + super.key, + required this.voidedOrder, + required this.voidType, + required this.voidAmount, + this.voidedItems, + required this.voidReason, + }); + + @override + State createState() => _SuccessVoidPageState(); +} + +class _SuccessVoidPageState extends State + with TickerProviderStateMixin { + final Color primaryColor = Color(0xFF36175E); + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _setupAnimations(); + _startAnimations(); + } + + void _setupAnimations() { + _animationController = AnimationController( + duration: Duration(milliseconds: 1500), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.elasticOut, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.3, 1.0, curve: Curves.easeInOut), + )); + + _slideAnimation = Tween( + begin: Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.5, 1.0, curve: Curves.easeOutCubic), + )); + } + + void _startAnimations() { + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + body: Stack( + children: [ + _buildBackgroundPattern(), + SafeArea( + child: Padding( + padding: EdgeInsets.all(24.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left Panel - Success Animation & Message + Flexible( + flex: 2, + child: _buildSuccessPanel(), + ), + SizedBox(width: 24), + // Right Panel - Void Details (FIXED SCROLL) + Flexible( + flex: 3, + child: _buildDetailsPanel(), + ), + ], + ), + ), + ), + // REMOVED: Confetti overlay yang mengganggu scroll + ], + ), + ); + } + + Widget _buildSuccessPanel() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Success Icon with Animation + ScaleTransition( + scale: _scaleAnimation, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.green.shade400, Colors.green.shade600], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.3), + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Icon( + Icons.check_rounded, + color: Colors.white, + size: 60, + ), + ), + ), + + SizedBox(height: 32), + + // Success Title + FadeTransition( + opacity: _fadeAnimation, + child: Text( + 'Void Berhasil!', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + textAlign: TextAlign.center, + ), + ), + + SizedBox(height: 16), + + // Success Message + SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Text( + widget.voidType == 'all' + ? 'Seluruh pesanan telah dibatalkan dengan sukses' + : 'Item terpilih telah dibatalkan dengan sukses', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + ), + + SizedBox(height: 32), + + // Order Info Card + SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: primaryColor.withOpacity(0.2)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.receipt_long, + color: primaryColor, + size: 20, + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pesanan #${widget.voidedOrder.orderNumber}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: primaryColor, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + 'Meja: ${widget.voidedOrder.tableNumber ?? 'N/A'}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + if (widget.voidAmount > 0) ...[ + SizedBox(height: 16), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Jumlah Void:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + Text( + _formatCurrency(widget.voidAmount), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.red.shade700, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + + // FIXED: Panel kanan dengan scroll yang benar + Widget _buildDetailsPanel() { + return LayoutBuilder( + builder: (context, constraints) { + return Container( + height: constraints.maxHeight, // PENTING: Gunakan height dari parent + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header - Fixed + Container( + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.info_outline, + color: primaryColor, + size: 24, + ), + ), + SizedBox(width: 12), + Expanded( + child: Text( + 'Detail Void', + style: TextStyle( + color: primaryColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + + // FIXED: Scrollable Content + Expanded( + child: Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Void Type Info + _buildInfoCard( + icon: Icons.category, + title: 'Tipe Void', + content: widget.voidType == 'all' + ? 'Seluruh Pesanan' + : 'Item Tertentu', + ), + + SizedBox(height: 16), + + // Void Reason + _buildInfoCard( + icon: Icons.note_alt, + title: 'Alasan Void', + content: widget.voidReason, + ), + + SizedBox(height: 16), + + // Voided Items (if item void) + if (widget.voidType == 'item' && + widget.voidedItems != null) ...[ + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + Icons.list_alt, + color: Colors.grey.shade700, + size: 18, + ), + ), + SizedBox(width: 8), + Expanded( + child: Text( + 'Item yang Divoid', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 12), + // Items list (tidak nested scroll) + ...widget.voidedItems! + .map((item) => _buildVoidedItemCard(item)) + .toList(), + ], + ), + ), + SizedBox(height: 16), + ], + + // Timestamp + _buildInfoCard( + icon: Icons.access_time, + title: 'Waktu Void', + content: _formatDateTime(DateTime.now()), + ), + + SizedBox(height: 32), + ], + ), + ), + ), + ), + + Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 20, + ), + child: Row( + children: [ + Expanded( + child: Button.outlined( + height: 50, + onPressed: () { + context.pushReplacement(DashboardPage()); + }, + icon: Icon(Icons.home, size: 20), + label: 'Kembali ke Beranda', + ), + ), + SpaceWidth(12), + Expanded( + child: Button.filled( + height: 50, + onPressed: _printVoidReceipt, + icon: Icon(Icons.print, size: 20), + label: 'Cetak Struk', + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildInfoCard({ + required IconData icon, + required String title, + required String content, + }) { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: Colors.grey.shade700, + size: 20, + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 4), + Text( + content, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildVoidedItemCard(OrderItem item) { + return Container( + margin: EdgeInsets.only(bottom: 8), + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.grey.shade400, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.productName ?? 'Unknown Product', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + 'Qty: ${item.quantity}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + _formatCurrency(item.totalPrice ?? 0), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ); + } + + Widget _buildBackgroundPattern() { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey.shade50, + Colors.grey.shade100, + ], + ), + ), + child: CustomPaint( + painter: PatternPainter(), + size: Size.infinite, + ), + ); + } + + String _formatCurrency(int amount) { + return 'Rp ${amount.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}'; + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } + + void _printVoidReceipt() { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Struk void sedang dicetak...'), + backgroundColor: primaryColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } +} + +// Simplified Pattern Painter (tidak mengganggu performa) +class PatternPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.05) + ..strokeWidth = 1; + + // Simplified pattern - tidak terlalu banyak + for (int i = 0; i < size.width; i += 100) { + for (int j = 0; j < size.height; j += 100) { + canvas.drawCircle(Offset(i.toDouble(), j.toDouble()), 1, paint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/presentation/void/pages/void_page.dart b/lib/presentation/void/pages/void_page.dart index c64a7c9..5cbbf7c 100644 --- a/lib/presentation/void/pages/void_page.dart +++ b/lib/presentation/void/pages/void_page.dart @@ -6,6 +6,7 @@ import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart'; import 'package:enaklo_pos/presentation/void/bloc/void_order_bloc.dart'; import 'package:enaklo_pos/presentation/void/dialog/confirm_void_dialog.dart'; +import 'package:enaklo_pos/presentation/void/pages/success_void_page.dart'; import 'package:enaklo_pos/presentation/void/widgets/product_card.dart'; import 'package:enaklo_pos/presentation/void/widgets/void_radio.dart'; import 'package:enaklo_pos/presentation/void/widgets/void_loading.dart'; @@ -47,7 +48,15 @@ class _VoidPageState extends State { // Show loading indicator if needed }, success: () { - _showSuccessDialog(); + context.pushReplacement( + SuccessVoidPage( + voidedOrder: widget.selectedOrder, + voidType: voidType, + voidAmount: _calculateVoidAmount(), + voidReason: voidReason, + voidedItems: voidType == 'item' ? _getVoidedItems() : null, + ), + ); }, error: (message) { _showErrorDialog(message); @@ -231,7 +240,7 @@ class _VoidPageState extends State { color: AppColors.primary, size: 20), SpaceWidth(8), Text( - 'Produk Pesanan (${widget.selectedOrder.orderItems?.length ?? 0})', + 'Produk Pesanan (${_getPendingItem().length})', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -245,9 +254,9 @@ class _VoidPageState extends State { // Order Items List - Scrollable ...List.generate( - widget.selectedOrder.orderItems?.length ?? 0, + _getPendingItem().length, (index) { - final item = widget.selectedOrder.orderItems![index]; + final item = _getPendingItem()[index]; final voidQty = selectedItemQuantities[item.id] ?? 0; final isSelected = voidQty > 0; final canSelect = voidType == 'item'; @@ -680,6 +689,41 @@ class _VoidPageState extends State { return true; } + List _getPendingItem() { + return widget.selectedOrder.orderItems! + .where((item) => item.status == 'pending') + .toList(); + } + + List _getVoidedItems() { + List voidedItems = []; + + selectedItemQuantities.forEach((itemId, voidQty) { + final originalItem = widget.selectedOrder.orderItems! + .firstWhere((item) => item.id == itemId); + + // Buat OrderItem baru dengan quantity yang di-void + voidedItems.add(OrderItem( + id: originalItem.id, + orderId: originalItem.orderId, + productId: originalItem.productId, + productName: originalItem.productName, + productVariantId: originalItem.productVariantId, + productVariantName: originalItem.productVariantName, + quantity: voidQty, // Hanya quantity yang di-void + unitPrice: originalItem.unitPrice, + totalPrice: (originalItem.unitPrice ?? 0) * voidQty, + modifiers: originalItem.modifiers, + notes: originalItem.notes, + status: originalItem.status, + createdAt: originalItem.createdAt, + updatedAt: originalItem.updatedAt, + )); + }); + + return voidedItems; + } + void _processVoid() { String confirmMessage; if (voidType == 'all') { @@ -748,42 +792,6 @@ class _VoidPageState extends State { ); } - void _showSuccessDialog() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.check_circle, color: Colors.green), - SpaceWidth(8), - Text('Void Berhasil'), - ], - ), - content: Text( - voidType == 'all' - ? 'Pesanan #${widget.selectedOrder.orderNumber} telah berhasil dibatalkan.' - : 'Produk yang dipilih dari pesanan #${widget.selectedOrder.orderNumber} telah berhasil dibatalkan.\n\nJumlah yang Dibatalkan: ${(_calculateVoidAmount()).currencyFormatRpV2}', - ), - actions: [ - ElevatedButton( - onPressed: () { - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Go back to previous screen - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - ), - child: Text('OK'), - ), - ], - ); - }, - ); - } - void _showErrorDialog(String message) { showDialog( context: context,