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' ),
),
],
);
},
);
}
}