2025-08-04 20:40:25 +07:00
import ' package:enaklo_pos/core/components/buttons.dart ' ;
import ' package:enaklo_pos/core/components/spaces.dart ' ;
import ' package:enaklo_pos/core/constants/colors.dart ' ;
import ' package:enaklo_pos/core/extensions/build_context_ext.dart ' ;
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 ' ;
2025-08-04 21:33:27 +07:00
import ' package:enaklo_pos/presentation/void/pages/success_void_page.dart ' ;
2025-08-04 20:40:25 +07:00
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 ' ;
import ' package:flutter/material.dart ' ;
import ' package:flutter_bloc/flutter_bloc.dart ' ;
class VoidPage extends StatefulWidget {
final Order selectedOrder ;
const VoidPage ( { super . key , required this . selectedOrder } ) ;
@ override
State < VoidPage > createState ( ) = > _VoidPageState ( ) ;
}
class _VoidPageState extends State < VoidPage > {
String voidType = ' all ' ; // 'all' or 'item'
Map < String , int > selectedItemQuantities = { } ; // itemId -> voidQuantity
String voidReason = ' ' ;
final TextEditingController reasonController = TextEditingController ( ) ;
final ScrollController _leftPanelController = ScrollController ( ) ;
final ScrollController _rightPanelController = ScrollController ( ) ;
@ override
void dispose ( ) {
_leftPanelController . dispose ( ) ;
_rightPanelController . dispose ( ) ;
reasonController . dispose ( ) ;
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
return BlocListener < VoidOrderBloc , VoidOrderState > (
listener: ( context , state ) {
state . when (
initial: ( ) { } ,
loading: ( ) {
// Show loading indicator if needed
} ,
success: ( ) {
2025-08-04 21:33:27 +07:00
context . pushReplacement (
SuccessVoidPage (
voidedOrder: widget . selectedOrder ,
voidType: voidType ,
voidAmount: _calculateVoidAmount ( ) ,
voidReason: voidReason ,
voidedItems: voidType = = ' item ' ? _getVoidedItems ( ) : null ,
) ,
) ;
2025-08-04 20:40:25 +07:00
} ,
error: ( message ) {
_showErrorDialog ( message ) ;
} ,
) ;
} ,
child: Scaffold (
backgroundColor: Colors . grey [ 100 ] ,
body: BlocBuilder < VoidOrderBloc , VoidOrderState > (
builder: ( context , state ) {
return Stack (
children: [
OrientationBuilder (
builder: ( context , orientation ) {
return Padding (
padding: EdgeInsets . all ( 24.0 ) ,
child: Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Left Panel - Order Details & Items
Expanded (
flex: 3 ,
child: _buildOrderDetailsPanel ( ) ,
) ,
SpaceWidth ( 24 ) ,
// Right Panel - Void Configuration
Expanded (
flex: 2 ,
child: _buildVoidConfigPanel ( ) ,
) ,
] ,
) ,
) ;
} ,
) ,
// Loading Overlay
state . when (
initial: ( ) = > SizedBox . shrink ( ) ,
loading: ( ) = > VoidLoading ( ) ,
success: ( ) = > SizedBox . shrink ( ) ,
error: ( message ) = > SizedBox . shrink ( ) ,
) ,
] ,
) ;
} ,
) ,
) ,
) ;
}
Widget _buildOrderDetailsPanel ( ) {
return Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Order Header - Fixed
Container (
padding: EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
color: AppColors . primary . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . only (
topLeft: Radius . circular ( 12 ) ,
topRight: Radius . circular ( 12 ) ,
) ,
) ,
child: Row (
children: [
Icon ( Icons . receipt_long , color: AppColors . primary , size: 24 ) ,
SpaceWidth ( 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
' Pesanan # ${ widget . selectedOrder . orderNumber } ' ,
style: TextStyle (
fontSize: 20 ,
fontWeight: FontWeight . bold ,
color: AppColors . primary ,
) ,
) ,
Text (
' Meja: ${ widget . selectedOrder . tableNumber ? ? ' N/A ' } • ${ widget . selectedOrder . orderType ? ? ' N/A ' } ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . grey [ 600 ] ,
) ,
) ,
] ,
) ,
) ,
Container (
padding: EdgeInsets . symmetric ( horizontal: 12 , vertical: 6 ) ,
decoration: BoxDecoration (
color: _getStatusColor ( widget . selectedOrder . status )
. withOpacity ( 0.2 ) ,
borderRadius: BorderRadius . circular ( 16 ) ,
) ,
child: Text (
widget . selectedOrder . status ? . toUpperCase ( ) ? ? ' UNKNOWN ' ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . w600 ,
color: _getStatusColor ( widget . selectedOrder . status ) ,
) ,
) ,
) ,
] ,
) ,
) ,
// Scrollable Content
Expanded (
child: Scrollbar (
controller: _leftPanelController ,
thumbVisibility: true ,
child: SingleChildScrollView (
controller: _leftPanelController ,
padding: EdgeInsets . all ( 20 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Order Summary - Fixed in scroll
Container (
padding: EdgeInsets . all ( 16 ) ,
decoration: BoxDecoration (
color: Colors . grey [ 50 ] ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: Colors . grey [ 300 ] ! ) ,
) ,
child: Column (
children: [
_buildSummaryRow (
' Subtotal: ' ,
( widget . selectedOrder . subtotal ? ? 0 )
. currencyFormatRpV2 ) ,
_buildSummaryRow (
' Pajak: ' ,
( widget . selectedOrder . taxAmount ? ? 0 )
. currencyFormatRpV2 ) ,
_buildSummaryRow ( ' Diskon: ' ,
' - ${ ( widget . selectedOrder . discountAmount ? ? 0 ) . currencyFormatRpV2 } ' ) ,
Divider ( thickness: 1 ) ,
_buildSummaryRow (
' Total: ' ,
( widget . selectedOrder . totalAmount ? ? 0 )
. currencyFormatRpV2 ,
isTotal: true ,
) ,
if ( voidType = = ' item ' & &
selectedItemQuantities . isNotEmpty ) . . . [
Divider ( thickness: 1 , color: Colors . red [ 300 ] ) ,
_buildSummaryRow (
' Total Void: ' ,
' - ${ ( _calculateVoidAmount ( ) . currencyFormatRpV2 ) } ' ,
isVoid: true ,
) ,
] ,
] ,
) ,
) ,
SpaceHeight ( 24 ) ,
// Order Items Section Title
Row (
children: [
Icon ( Icons . shopping_cart ,
color: AppColors . primary , size: 20 ) ,
SpaceWidth ( 8 ) ,
Text (
2025-08-04 21:33:27 +07:00
' Produk Pesanan ( ${ _getPendingItem ( ) . length } ) ' ,
2025-08-04 20:40:25 +07:00
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
color: AppColors . primary ,
) ,
) ,
] ,
) ,
SpaceHeight ( 16 ) ,
// Order Items List - Scrollable
. . . List . generate (
2025-08-04 21:33:27 +07:00
_getPendingItem ( ) . length ,
2025-08-04 20:40:25 +07:00
( index ) {
2025-08-04 21:33:27 +07:00
final item = _getPendingItem ( ) [ index ] ;
2025-08-04 20:40:25 +07:00
final voidQty = selectedItemQuantities [ item . id ] ? ? 0 ;
final isSelected = voidQty > 0 ;
final canSelect = voidType = = ' item ' ;
return VoidProductCard (
isSelected: isSelected ,
item: item ,
voidQty: voidQty ,
canSelect: canSelect ,
onTapDecrease: voidQty > 0
? ( ) {
setState ( ( ) {
if ( voidQty = = 1 ) {
selectedItemQuantities . remove ( item . id ) ;
} else {
selectedItemQuantities [ item . id ! ] =
voidQty - 1 ;
}
} ) ;
}
: null ,
onTapIncrease: voidQty < ( item . quantity ? ? 0 )
? ( ) {
setState ( ( ) {
selectedItemQuantities [ item . id ! ] =
voidQty + 1 ;
} ) ;
}
: null ,
onTapAll: ( ) {
setState ( ( ) {
selectedItemQuantities [ item . id ! ] =
item . quantity ? ? 0 ;
} ) ;
} ,
onTapClear: voidQty > 0
? ( ) {
setState ( ( ) {
selectedItemQuantities . remove ( item . id ) ;
} ) ;
}
: null ,
) ;
} ,
) ,
] ,
) ,
) ,
) ,
) ,
] ,
) ,
) ;
}
Widget _buildVoidConfigPanel ( ) {
return Container (
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Header - Fixed
Container (
padding: EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
color: Colors . red . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . only (
topLeft: Radius . circular ( 12 ) ,
topRight: Radius . circular ( 12 ) ,
) ,
) ,
child: Row (
children: [
Icon ( Icons . cancel , color: Colors . red , size: 24 ) ,
SpaceWidth ( 12 ) ,
Text (
' Konfigurasi Void ' ,
style: TextStyle (
fontSize: 20 ,
fontWeight: FontWeight . bold ,
color: Colors . red ,
) ,
) ,
] ,
) ,
) ,
// Scrollable Content
Expanded (
child: Scrollbar (
controller: _rightPanelController ,
thumbVisibility: true ,
child: SingleChildScrollView (
controller: _rightPanelController ,
padding: EdgeInsets . all ( 20 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Void Type Selection
Text (
' Tipe Void * ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
color: AppColors . primary ,
) ,
) ,
SpaceHeight ( 12 ) ,
// Void All Option
VoidRadio (
voidType: voidType ,
value: ' all ' ,
title: ' Batalkan Seluruh Pesanan ' ,
subtitle: " Batalkan pesanan lengkap dan semua item " ,
onChanged: ( String ? value ) {
setState ( ( ) {
voidType = value ! ;
selectedItemQuantities . clear ( ) ;
} ) ;
} ,
) ,
SpaceHeight ( 12 ) ,
// Void Items Option
VoidRadio (
voidType: voidType ,
value: ' item ' ,
title: ' Batalkan Barang/Jumlah Tertentu ' ,
subtitle:
" Mengurangi atau membatalkan jumlah item tertentu " ,
onChanged: ( String ? value ) {
setState ( ( ) {
voidType = value ! ;
selectedItemQuantities . clear ( ) ;
} ) ;
} ,
) ,
SpaceHeight ( 24 ) ,
// Selected Items Summary (only show for item void)
if ( voidType = = ' item ' ) . . . [
Container (
padding: EdgeInsets . all ( 16 ) ,
decoration: BoxDecoration (
color: selectedItemQuantities . isEmpty
? Colors . orange [ 50 ]
: Colors . green [ 50 ] ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all (
color: selectedItemQuantities . isEmpty
? Colors . orange [ 300 ] !
: Colors . green [ 300 ] ! ,
) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Icon (
selectedItemQuantities . isEmpty
? Icons . warning
: Icons . check_circle ,
color: selectedItemQuantities . isEmpty
? Colors . orange [ 700 ]
: Colors . green [ 700 ] ,
size: 20 ,
) ,
SpaceWidth ( 8 ) ,
Expanded (
child: Text (
selectedItemQuantities . isEmpty
? ' Silakan pilih item dan jumlah yang akan dibatalkan '
: ' Item yang dipilih untuk dibatalkan: ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w500 ,
color: selectedItemQuantities . isEmpty
? Colors . orange [ 700 ]
: Colors . green [ 700 ] ,
) ,
) ,
) ,
] ,
) ,
if ( selectedItemQuantities . isNotEmpty ) . . . [
SpaceHeight ( 12 ) ,
Container (
constraints: BoxConstraints ( maxHeight: 150 ) ,
child: Scrollbar (
child: SingleChildScrollView (
child: Column (
crossAxisAlignment:
CrossAxisAlignment . start ,
children: selectedItemQuantities . entries
. map ( ( entry ) {
final item = widget
. selectedOrder . orderItems !
. firstWhere (
( item ) = > item . id = = entry . key ) ;
return Container (
margin: EdgeInsets . only ( bottom: 8 ) ,
padding: EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius:
BorderRadius . circular ( 6 ) ,
border: Border . all (
color: Colors . green [ 200 ] ! ) ,
) ,
child: Row (
children: [
Expanded (
child: Column (
crossAxisAlignment:
CrossAxisAlignment . start ,
children: [
Text (
item . productName ? ?
' Unknown ' ,
style: TextStyle (
fontSize: 12 ,
fontWeight:
FontWeight . w500 ,
) ,
) ,
Text (
' Qty: ${ entry . value } ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . grey [ 600 ] ,
) ,
) ,
] ,
) ,
) ,
Text (
( ( item . unitPrice ? ? 0 ) *
entry . value )
. currencyFormatRpV2 ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . bold ,
color: Colors . red [ 700 ] ,
) ,
) ,
] ,
) ,
) ;
} ) . toList ( ) ,
) ,
) ,
) ,
) ,
Divider ( height: 16 , thickness: 1 ) ,
Container (
padding: EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . red . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . circular ( 6 ) ,
) ,
child: Row (
mainAxisAlignment:
MainAxisAlignment . spaceBetween ,
children: [
Text (
' Jumlah Total Void: ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . bold ,
) ,
) ,
Text (
_calculateVoidAmount ( ) . currencyFormatRpV2 ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . bold ,
color: Colors . red [ 700 ] ,
) ,
) ,
] ,
) ,
) ,
] ,
] ,
) ,
) ,
SpaceHeight ( 24 ) ,
] ,
// Void Reason
Text (
' Alasan Void * ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
color: AppColors . primary ,
) ,
) ,
SpaceHeight ( 8 ) ,
TextField (
controller: reasonController ,
maxLines: 4 ,
decoration: InputDecoration (
hintText: ' Harap berikan alasan untuk membatalkan... ' ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
borderSide: BorderSide ( color: Colors . grey [ 300 ] ! ) ,
) ,
focusedBorder: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
borderSide:
BorderSide ( color: AppColors . primary , width: 2 ) ,
) ,
contentPadding: EdgeInsets . all ( 16 ) ,
) ,
onChanged: ( value ) {
setState ( ( ) {
voidReason = value ;
} ) ;
} ,
) ,
SpaceHeight ( 32 ) ,
// Action Buttons
Row (
children: [
Expanded (
child: Button . outlined (
onPressed: ( ) = > context . pop ( ) ,
label: ' Batal ' ,
) ,
) ,
SpaceWidth ( 12 ) ,
Expanded (
child: Button . filled (
onPressed: _canProcessVoid ( ) ? _processVoid : null ,
label: voidType = = ' all '
? ' Void Pesanan '
: ' Void Produk ' ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
) ,
) ,
] ,
) ,
) ;
}
Widget _buildSummaryRow ( String label , String value ,
{ bool isTotal = false , bool isVoid = false } ) {
return Padding (
padding: EdgeInsets . symmetric ( vertical: 4 ) ,
child: Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text (
label ,
style: TextStyle (
fontSize: isTotal ? 16 : 14 ,
fontWeight: isTotal ? FontWeight . bold : FontWeight . normal ,
color: isVoid
? Colors . red
: ( isTotal ? AppColors . primary : Colors . grey [ 700 ] ) ,
) ,
) ,
Text (
value ,
style: TextStyle (
fontSize: isTotal ? 16 : 14 ,
fontWeight: isTotal ? FontWeight . bold : FontWeight . w500 ,
color: isVoid
? Colors . red
: ( isTotal ? AppColors . primary : Colors . grey [ 700 ] ) ,
) ,
) ,
] ,
) ,
) ;
}
Color _getStatusColor ( String ? status ) {
switch ( status ? . toLowerCase ( ) ) {
case ' completed ' :
return Colors . green ;
case ' pending ' :
return Colors . orange ;
case ' cancelled ' :
return Colors . red ;
case ' processing ' :
return Colors . blue ;
default :
return Colors . grey ;
}
}
int _calculateVoidAmount ( ) {
int total = 0 ;
selectedItemQuantities . forEach ( ( itemId , voidQty ) {
final item = widget . selectedOrder . orderItems !
. firstWhere ( ( item ) = > item . id = = itemId ) ;
total + = ( item . unitPrice ? ? 0 ) * voidQty ;
} ) ;
return total ;
}
bool _canProcessVoid ( ) {
if ( voidReason . trim ( ) . isEmpty ) return false ;
if ( voidType = = ' item ' & & selectedItemQuantities . isEmpty ) return false ;
return true ;
}
2025-08-04 21:33:27 +07:00
List < OrderItem > _getPendingItem ( ) {
return widget . selectedOrder . orderItems !
. where ( ( item ) = > item . status = = ' pending ' )
. toList ( ) ;
}
List < OrderItem > _getVoidedItems ( ) {
List < OrderItem > 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 ;
}
2025-08-04 20:40:25 +07:00
void _processVoid ( ) {
String confirmMessage ;
if ( voidType = = ' all ' ) {
confirmMessage =
' Apakah Anda yakin ingin membatalkan seluruh pesanan # ${ widget . selectedOrder . orderNumber } ? \n \n Ini akan membatalkan semua item dalam pesanan. ' ;
} else {
int totalItems =
selectedItemQuantities . values . fold ( 0 , ( sum , qty ) = > sum + qty ) ;
confirmMessage =
' Apakah Anda yakin ingin membatalkan $ totalItems item dari pesanan # ${ widget . selectedOrder . orderNumber } ? \n \n Jumlah Batal: ${ ( _calculateVoidAmount ( ) ) . currencyFormatRpV2 } ' ;
}
showDialog (
context: context ,
barrierDismissible: false ,
builder: ( BuildContext context ) {
return ConfirmVoidDialog (
message: confirmMessage ,
onTap: _performVoid ,
order: widget . selectedOrder ,
voidType: voidType ,
selectedItemQuantities: selectedItemQuantities ,
voidReason: voidReason ,
) ;
} ,
) ;
}
void _performVoid ( ) {
// Prepare order items for void
List < OrderItem > voidItems = [ ] ;
if ( voidType = = ' item ' ) {
selectedItemQuantities . forEach ( ( itemId , voidQty ) {
final originalItem = widget . selectedOrder . orderItems !
. firstWhere ( ( item ) = > item . id = = itemId ) ;
// Create new OrderItem with void quantity
voidItems . add ( OrderItem (
id: originalItem . id ,
orderId: originalItem . orderId ,
productId: originalItem . productId ,
productName: originalItem . productName ,
productVariantId: originalItem . productVariantId ,
productVariantName: originalItem . productVariantName ,
quantity: voidQty , // This is the void quantity
unitPrice: originalItem . unitPrice ,
totalPrice: ( originalItem . unitPrice ? ? 0 ) * voidQty ,
modifiers: originalItem . modifiers ,
notes: originalItem . notes ,
status: originalItem . status ,
createdAt: originalItem . createdAt ,
updatedAt: originalItem . updatedAt ,
) ) ;
} ) ;
}
// Trigger void order event
context . read < VoidOrderBloc > ( ) . add (
VoidOrderEvent . voidOrder (
orderId: widget . selectedOrder . id ! ,
reason: voidReason ,
type: voidType . toUpperCase ( ) ,
orderItems: voidItems ,
) ,
) ;
}
void _showErrorDialog ( String message ) {
showDialog (
context: context ,
builder: ( BuildContext context ) {
return AlertDialog (
title: Row (
children: [
Icon ( Icons . error , color: Colors . red ) ,
SpaceWidth ( 8 ) ,
Text ( ' Void Gagal ' ) ,
] ,
) ,
content: Text ( message ) ,
actions: [
ElevatedButton (
onPressed: ( ) = > Navigator . pop ( context ) ,
style: ElevatedButton . styleFrom (
backgroundColor: AppColors . primary ,
foregroundColor: Colors . white ,
) ,
child: Text ( ' OK ' ) ,
) ,
] ,
) ;
} ,
) ;
}
}