feat: void page

This commit is contained in:
efrilm 2025-08-04 20:40:25 +07:00
parent 835c834ce1
commit 80649fe082
13 changed files with 2885 additions and 135 deletions

View File

@ -43,7 +43,7 @@ class Button extends StatelessWidget {
this.crossAxisAlignment = CrossAxisAlignment.center, this.crossAxisAlignment = CrossAxisAlignment.center,
}); });
final Function() onPressed; final Function()? onPressed;
final String label; final String label;
final ButtonStyle style; final ButtonStyle style;
final Color color; final Color color;

View File

@ -11,6 +11,7 @@ import 'package:enaklo_pos/presentation/home/bloc/outlet_loader/outlet_loader_bl
import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart';
import 'package:enaklo_pos/presentation/void/bloc/void_order_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart';
@ -252,6 +253,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider( BlocProvider(
create: (context) => CurrentOutletBloc(OutletRemoteDataSource()), create: (context) => CurrentOutletBloc(OutletRemoteDataSource()),
), ),
BlocProvider(
create: (context) => VoidOrderBloc(OrderRemoteDatasource()),
),
], ],
child: MaterialApp( child: MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@ -1,13 +1,14 @@
import 'package:enaklo_pos/core/components/buttons.dart'; import 'package:enaklo_pos/core/components/buttons.dart';
import 'package:enaklo_pos/core/components/flushbar.dart'; import 'package:enaklo_pos/core/components/flushbar.dart';
import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart';
import 'package:enaklo_pos/presentation/sales/dialog/payment_dialog.dart'; import 'package:enaklo_pos/presentation/sales/dialog/payment_dialog.dart';
import 'package:enaklo_pos/presentation/sales/dialog/refund_dialog.dart'; import 'package:enaklo_pos/presentation/sales/dialog/refund_dialog.dart';
import 'package:enaklo_pos/presentation/sales/dialog/void_dialog.dart'; import 'package:enaklo_pos/presentation/void/pages/void_page.dart';
import 'package:enaklo_pos/presentation/sales/widgets/sales_detail.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_detail.dart';
import 'package:enaklo_pos/presentation/sales/widgets/sales_list_order.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_list_order.dart';
import 'package:enaklo_pos/presentation/sales/widgets/sales_order_information.dart'; import 'package:enaklo_pos/presentation/sales/widgets/sales_order_information.dart';
@ -183,13 +184,18 @@ class _SalesPageState extends State<SalesPage> {
loaded: (order, selectedItems, loaded: (order, selectedItems,
totalVoidOrRefund, isAllSelected) => totalVoidOrRefund, isAllSelected) =>
Button.outlined( Button.outlined(
onPressed: () => showDialog( onPressed: () {
context: context, context.push(VoidPage(
builder: (context) => VoidDialog( selectedOrder: order,
order: orderDetail!, ));
selectedItems: selectedItems, // showDialog(
), // context: context,
), // builder: (context) => VoidDialog(
// order: orderDetail!,
// selectedItems: selectedItems,
// ),
// );
},
label: 'Void', label: 'Void',
icon: Icon(Icons.undo), icon: Icon(Icons.undo),
), ),

View File

@ -1,6 +1,5 @@
import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.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/string_ext.dart'; import 'package:enaklo_pos/core/extensions/string_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart'; import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
@ -17,7 +16,7 @@ class SalesListOrder extends StatelessWidget {
margin: const EdgeInsets.only(top: 16), margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.white, color: AppColors.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(16),
), ),
child: BlocBuilder<OrderFormBloc, OrderFormState>( child: BlocBuilder<OrderFormBloc, OrderFormState>(
builder: (context, state) { builder: (context, state) {
@ -27,51 +26,10 @@ class SalesListOrder extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( _buildHeader(context, isAllSelected),
padding: const EdgeInsets.all(16), const SpaceHeight(8),
width: double.infinity, _buildItemsList(context, selectedItems),
decoration: const BoxDecoration( const SpaceHeight(8),
border: Border(
bottom: BorderSide(color: AppColors.background),
),
),
child: Row(
children: [
Checkbox(
value: isAllSelected,
activeColor: AppColors.primary,
onChanged: (val) {
context.read<OrderFormBloc>().add(
OrderFormEvent.toggleSelectAll(val ?? false));
},
),
Text(
'Daftar Pembelian',
style: TextStyle(
color: AppColors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
SpaceHeight(12),
Column(
children: List.generate(
order?.orderItems?.length ?? 0,
(index) {
final item = order!.orderItems![index];
final isSelected =
selectedItems.any((e) => e.id == item.id);
return _item(
context,
isSelected,
item,
);
},
).toList(),
),
], ],
), ),
); );
@ -80,96 +38,345 @@ class SalesListOrder extends StatelessWidget {
); );
} }
Padding _item(BuildContext context, bool isSelected, OrderItem product) { Widget _buildHeader(BuildContext context, bool isAllSelected) {
return Padding( return Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16) padding: const EdgeInsets.all(20),
.copyWith(top: 0), decoration: BoxDecoration(
child: Column( gradient: LinearGradient(
colors: [
AppColors.primary.withOpacity(0.1),
AppColors.primary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
border: Border(
bottom: BorderSide(
color: AppColors.primary.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [ children: [
Align( Container(
alignment: Alignment.centerRight, decoration: BoxDecoration(
child: Text( color: AppColors.white,
product.status == "pending" borderRadius: BorderRadius.circular(8),
? "Pending" border: Border.all(
: product.status == "cancelled" color: isAllSelected ? AppColors.primary : Colors.grey.shade300,
? "Batal" width: 2,
: product.status == 'refund'
? "Refund"
: "Selesai",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: product.status == "pending"
? Colors.blue
: product.status == "cancelled"
? Colors.red
: product.status == 'refund'
? Colors.red
: Colors.green,
), ),
), ),
child: Checkbox(
value: isAllSelected,
activeColor: AppColors.primary,
checkColor: AppColors.white,
onChanged: (val) {
context
.read<OrderFormBloc>()
.add(OrderFormEvent.toggleSelectAll(val ?? false));
},
),
), ),
Row( const SizedBox(width: 12),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Expanded(
children: [ child: Column(
SizedBox( crossAxisAlignment: CrossAxisAlignment.start,
width: context.deviceWidth * 0.2, children: [
child: Row( Text(
children: [ 'Daftar Pembelian',
Checkbox( style: TextStyle(
value: isSelected, color: AppColors.black,
activeColor: AppColors.primary, fontSize: 18,
onChanged: (_) { fontWeight: FontWeight.w700,
context letterSpacing: -0.5,
.read<OrderFormBloc>() ),
.add(OrderFormEvent.toggleItem(product)); ),
}, const SizedBox(height: 4),
Text(
'${order?.orderItems?.length ?? 0} item',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.primary.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.shopping_cart_outlined,
size: 16,
color: AppColors.primary,
),
const SizedBox(width: 4),
Text(
'Order',
style: TextStyle(
color: AppColors.primary,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
Widget _buildItemsList(BuildContext context, List selectedItems) {
return Column(
children: List.generate(
order?.orderItems?.length ?? 0,
(index) {
final item = order!.orderItems![index];
final isSelected = selectedItems.any((e) => e.id == item.id);
return _buildItem(context, isSelected, item, index);
},
).toList(),
);
}
Widget _buildItem(
BuildContext context, bool isSelected, OrderItem product, int index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color:
isSelected ? AppColors.primary.withOpacity(0.05) : AppColors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? AppColors.primary.withOpacity(0.3)
: Colors.grey.shade200,
width: isSelected ? 2 : 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color:
isSelected ? AppColors.primary : Colors.grey.shade300,
width: 1.5,
), ),
Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Checkbox(
children: [ value: isSelected,
Text( activeColor: AppColors.primary,
product.productName ?? '', checkColor: AppColors.white,
style: const TextStyle( onChanged: (_) {
fontSize: 14, context
fontWeight: FontWeight.w600, .read<OrderFormBloc>()
), .add(OrderFormEvent.toggleItem(product));
), },
if (product.productVariantName != null) ),
Text( ),
product.productVariantName ?? '', const SizedBox(width: 12),
style: const TextStyle( Expanded(
fontSize: 12, child: Column(
fontWeight: FontWeight.w600, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
product.productName ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
),
), ),
), ),
Text( _buildStatusBadge(product.status),
(product.unitPrice ?? 0) ],
.toString() ),
.currencyFormatRpV2, if (product.productVariantName != null) ...[
style: const TextStyle( const SizedBox(height: 4),
fontSize: 14, Container(
fontWeight: FontWeight.w500, padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Text(
product.productVariantName ?? '',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
), ),
), ),
], ],
), const SizedBox(height: 12),
], Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Harga Satuan',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
(product.unitPrice ?? 0)
.toString()
.currencyFormatRpV2,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'x${product.quantity}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Total',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
(product.totalPrice ?? 0)
.toString()
.currencyFormatRpV2,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
],
),
],
),
],
),
), ),
), ],
Text( ),
'X${product.quantity}', ],
style: const TextStyle( ),
fontSize: 14, ),
), );
), }
Text(
(product.totalPrice ?? 0).toString().currencyFormatRpV2, Widget _buildStatusBadge(String? status) {
style: const TextStyle( Color backgroundColor;
fontSize: 14, Color textColor;
), String displayText;
), IconData icon;
],
switch (status) {
case "pending":
backgroundColor = Colors.orange.withOpacity(0.1);
textColor = Colors.orange.shade700;
displayText = "Pending";
icon = Icons.access_time;
break;
case "cancelled":
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red.shade700;
displayText = "Batal";
icon = Icons.cancel_outlined;
break;
case "refund":
backgroundColor = Colors.purple.withOpacity(0.1);
textColor = Colors.purple.shade700;
displayText = "Refund";
icon = Icons.undo;
break;
default:
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green.shade700;
displayText = "Selesai";
icon = Icons.check_circle_outline;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: textColor.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 14,
color: textColor,
),
const SizedBox(width: 4),
Text(
displayText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: textColor,
),
), ),
], ],
), ),

View File

@ -0,0 +1,36 @@
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'void_order_event.dart';
part 'void_order_state.dart';
part 'void_order_bloc.freezed.dart';
class VoidOrderBloc extends Bloc<VoidOrderEvent, VoidOrderState> {
final OrderRemoteDatasource _orderRemoteDatasource;
VoidOrderBloc(this._orderRemoteDatasource)
: super(const VoidOrderState.initial()) {
on<_VoidOrder>(_onVoidOrder);
}
Future<void> _onVoidOrder(
_VoidOrder event,
Emitter<VoidOrderState> emit,
) async {
emit(const VoidOrderState.loading());
final result = await _orderRemoteDatasource.voidOrder(
orderId: event.orderId,
reason: event.reason,
type: event.type,
orderItems: event.orderItems,
);
result.fold(
(error) => emit(VoidOrderState.error(error)),
(success) => emit(const VoidOrderState.success()),
);
}
}

View File

@ -0,0 +1,889 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'void_order_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$VoidOrderEvent {
String get orderId => throw _privateConstructorUsedError;
String get reason => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError; // "ALL" or "ITEM"
List<OrderItem> get orderItems => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String orderId, String reason, String type,
List<OrderItem> orderItems)
voidOrder,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String orderId, String reason, String type,
List<OrderItem> orderItems)?
voidOrder,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String orderId, String reason, String type,
List<OrderItem> orderItems)?
voidOrder,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_VoidOrder value) voidOrder,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_VoidOrder value)? voidOrder,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_VoidOrder value)? voidOrder,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
/// Create a copy of VoidOrderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$VoidOrderEventCopyWith<VoidOrderEvent> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $VoidOrderEventCopyWith<$Res> {
factory $VoidOrderEventCopyWith(
VoidOrderEvent value, $Res Function(VoidOrderEvent) then) =
_$VoidOrderEventCopyWithImpl<$Res, VoidOrderEvent>;
@useResult
$Res call(
{String orderId, String reason, String type, List<OrderItem> orderItems});
}
/// @nodoc
class _$VoidOrderEventCopyWithImpl<$Res, $Val extends VoidOrderEvent>
implements $VoidOrderEventCopyWith<$Res> {
_$VoidOrderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of VoidOrderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? orderId = null,
Object? reason = null,
Object? type = null,
Object? orderItems = null,
}) {
return _then(_value.copyWith(
orderId: null == orderId
? _value.orderId
: orderId // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
orderItems: null == orderItems
? _value.orderItems
: orderItems // ignore: cast_nullable_to_non_nullable
as List<OrderItem>,
) as $Val);
}
}
/// @nodoc
abstract class _$$VoidOrderImplCopyWith<$Res>
implements $VoidOrderEventCopyWith<$Res> {
factory _$$VoidOrderImplCopyWith(
_$VoidOrderImpl value, $Res Function(_$VoidOrderImpl) then) =
__$$VoidOrderImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String orderId, String reason, String type, List<OrderItem> orderItems});
}
/// @nodoc
class __$$VoidOrderImplCopyWithImpl<$Res>
extends _$VoidOrderEventCopyWithImpl<$Res, _$VoidOrderImpl>
implements _$$VoidOrderImplCopyWith<$Res> {
__$$VoidOrderImplCopyWithImpl(
_$VoidOrderImpl _value, $Res Function(_$VoidOrderImpl) _then)
: super(_value, _then);
/// Create a copy of VoidOrderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? orderId = null,
Object? reason = null,
Object? type = null,
Object? orderItems = null,
}) {
return _then(_$VoidOrderImpl(
orderId: null == orderId
? _value.orderId
: orderId // ignore: cast_nullable_to_non_nullable
as String,
reason: null == reason
? _value.reason
: reason // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
orderItems: null == orderItems
? _value._orderItems
: orderItems // ignore: cast_nullable_to_non_nullable
as List<OrderItem>,
));
}
}
/// @nodoc
class _$VoidOrderImpl implements _VoidOrder {
const _$VoidOrderImpl(
{required this.orderId,
required this.reason,
required this.type,
required final List<OrderItem> orderItems})
: _orderItems = orderItems;
@override
final String orderId;
@override
final String reason;
@override
final String type;
// "ALL" or "ITEM"
final List<OrderItem> _orderItems;
// "ALL" or "ITEM"
@override
List<OrderItem> get orderItems {
if (_orderItems is EqualUnmodifiableListView) return _orderItems;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_orderItems);
}
@override
String toString() {
return 'VoidOrderEvent.voidOrder(orderId: $orderId, reason: $reason, type: $type, orderItems: $orderItems)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$VoidOrderImpl &&
(identical(other.orderId, orderId) || other.orderId == orderId) &&
(identical(other.reason, reason) || other.reason == reason) &&
(identical(other.type, type) || other.type == type) &&
const DeepCollectionEquality()
.equals(other._orderItems, _orderItems));
}
@override
int get hashCode => Object.hash(runtimeType, orderId, reason, type,
const DeepCollectionEquality().hash(_orderItems));
/// Create a copy of VoidOrderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$VoidOrderImplCopyWith<_$VoidOrderImpl> get copyWith =>
__$$VoidOrderImplCopyWithImpl<_$VoidOrderImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String orderId, String reason, String type,
List<OrderItem> orderItems)
voidOrder,
}) {
return voidOrder(orderId, reason, type, orderItems);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String orderId, String reason, String type,
List<OrderItem> orderItems)?
voidOrder,
}) {
return voidOrder?.call(orderId, reason, type, orderItems);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String orderId, String reason, String type,
List<OrderItem> orderItems)?
voidOrder,
required TResult orElse(),
}) {
if (voidOrder != null) {
return voidOrder(orderId, reason, type, orderItems);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_VoidOrder value) voidOrder,
}) {
return voidOrder(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_VoidOrder value)? voidOrder,
}) {
return voidOrder?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_VoidOrder value)? voidOrder,
required TResult orElse(),
}) {
if (voidOrder != null) {
return voidOrder(this);
}
return orElse();
}
}
abstract class _VoidOrder implements VoidOrderEvent {
const factory _VoidOrder(
{required final String orderId,
required final String reason,
required final String type,
required final List<OrderItem> orderItems}) = _$VoidOrderImpl;
@override
String get orderId;
@override
String get reason;
@override
String get type; // "ALL" or "ITEM"
@override
List<OrderItem> get orderItems;
/// Create a copy of VoidOrderEvent
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$VoidOrderImplCopyWith<_$VoidOrderImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$VoidOrderState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Loading value) loading,
required TResult Function(_Success value) success,
required TResult Function(_Error value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Loading value)? loading,
TResult? Function(_Success value)? success,
TResult? Function(_Error value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Loading value)? loading,
TResult Function(_Success value)? success,
TResult Function(_Error value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $VoidOrderStateCopyWith<$Res> {
factory $VoidOrderStateCopyWith(
VoidOrderState value, $Res Function(VoidOrderState) then) =
_$VoidOrderStateCopyWithImpl<$Res, VoidOrderState>;
}
/// @nodoc
class _$VoidOrderStateCopyWithImpl<$Res, $Val extends VoidOrderState>
implements $VoidOrderStateCopyWith<$Res> {
_$VoidOrderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$InitialImplCopyWith<$Res> {
factory _$$InitialImplCopyWith(
_$InitialImpl value, $Res Function(_$InitialImpl) then) =
__$$InitialImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$InitialImplCopyWithImpl<$Res>
extends _$VoidOrderStateCopyWithImpl<$Res, _$InitialImpl>
implements _$$InitialImplCopyWith<$Res> {
__$$InitialImplCopyWithImpl(
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
: super(_value, _then);
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$InitialImpl implements _Initial {
const _$InitialImpl();
@override
String toString() {
return 'VoidOrderState.initial()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$InitialImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Loading value) loading,
required TResult Function(_Success value) success,
required TResult Function(_Error value) error,
}) {
return initial(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Loading value)? loading,
TResult? Function(_Success value)? success,
TResult? Function(_Error value)? error,
}) {
return initial?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Loading value)? loading,
TResult Function(_Success value)? success,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial(this);
}
return orElse();
}
}
abstract class _Initial implements VoidOrderState {
const factory _Initial() = _$InitialImpl;
}
/// @nodoc
abstract class _$$LoadingImplCopyWith<$Res> {
factory _$$LoadingImplCopyWith(
_$LoadingImpl value, $Res Function(_$LoadingImpl) then) =
__$$LoadingImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$LoadingImplCopyWithImpl<$Res>
extends _$VoidOrderStateCopyWithImpl<$Res, _$LoadingImpl>
implements _$$LoadingImplCopyWith<$Res> {
__$$LoadingImplCopyWithImpl(
_$LoadingImpl _value, $Res Function(_$LoadingImpl) _then)
: super(_value, _then);
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$LoadingImpl implements _Loading {
const _$LoadingImpl();
@override
String toString() {
return 'VoidOrderState.loading()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$LoadingImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) {
return loading();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return loading?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Loading value) loading,
required TResult Function(_Success value) success,
required TResult Function(_Error value) error,
}) {
return loading(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Loading value)? loading,
TResult? Function(_Success value)? success,
TResult? Function(_Error value)? error,
}) {
return loading?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Loading value)? loading,
TResult Function(_Success value)? success,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading(this);
}
return orElse();
}
}
abstract class _Loading implements VoidOrderState {
const factory _Loading() = _$LoadingImpl;
}
/// @nodoc
abstract class _$$SuccessImplCopyWith<$Res> {
factory _$$SuccessImplCopyWith(
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
__$$SuccessImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$SuccessImplCopyWithImpl<$Res>
extends _$VoidOrderStateCopyWithImpl<$Res, _$SuccessImpl>
implements _$$SuccessImplCopyWith<$Res> {
__$$SuccessImplCopyWithImpl(
_$SuccessImpl _value, $Res Function(_$SuccessImpl) _then)
: super(_value, _then);
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$SuccessImpl implements _Success {
const _$SuccessImpl();
@override
String toString() {
return 'VoidOrderState.success()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$SuccessImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) {
return success();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return success?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (success != null) {
return success();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Loading value) loading,
required TResult Function(_Success value) success,
required TResult Function(_Error value) error,
}) {
return success(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Loading value)? loading,
TResult? Function(_Success value)? success,
TResult? Function(_Error value)? error,
}) {
return success?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Loading value)? loading,
TResult Function(_Success value)? success,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(this);
}
return orElse();
}
}
abstract class _Success implements VoidOrderState {
const factory _Success() = _$SuccessImpl;
}
/// @nodoc
abstract class _$$ErrorImplCopyWith<$Res> {
factory _$$ErrorImplCopyWith(
_$ErrorImpl value, $Res Function(_$ErrorImpl) then) =
__$$ErrorImplCopyWithImpl<$Res>;
@useResult
$Res call({String message});
}
/// @nodoc
class __$$ErrorImplCopyWithImpl<$Res>
extends _$VoidOrderStateCopyWithImpl<$Res, _$ErrorImpl>
implements _$$ErrorImplCopyWith<$Res> {
__$$ErrorImplCopyWithImpl(
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
: super(_value, _then);
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
}) {
return _then(_$ErrorImpl(
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$ErrorImpl implements _Error {
const _$ErrorImpl(this.message);
@override
final String message;
@override
String toString() {
return 'VoidOrderState.error(message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ErrorImpl &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, message);
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
__$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function() success,
required TResult Function(String message) error,
}) {
return error(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function()? success,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function()? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Loading value) loading,
required TResult Function(_Success value) success,
required TResult Function(_Error value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Loading value)? loading,
TResult? Function(_Success value)? success,
TResult? Function(_Error value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Loading value)? loading,
TResult Function(_Success value)? success,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
}
abstract class _Error implements VoidOrderState {
const factory _Error(final String message) = _$ErrorImpl;
String get message;
/// Create a copy of VoidOrderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,11 @@
part of 'void_order_bloc.dart';
@freezed
class VoidOrderEvent with _$VoidOrderEvent {
const factory VoidOrderEvent.voidOrder({
required String orderId,
required String reason,
required String type, // "ALL" or "ITEM"
required List<OrderItem> orderItems,
}) = _VoidOrder;
}

View File

@ -0,0 +1,9 @@
part of 'void_order_bloc.dart';
@freezed
class VoidOrderState with _$VoidOrderState {
const factory VoidOrderState.initial() = _Initial;
const factory VoidOrderState.loading() = _Loading;
const factory VoidOrderState.success() = _Success;
const factory VoidOrderState.error(String message) = _Error;
}

View File

@ -0,0 +1,397 @@
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:flutter/material.dart';
class ConfirmVoidDialog extends StatelessWidget {
final String message;
final String voidType;
final Order order;
final Map<String, int> selectedItemQuantities;
final String voidReason;
final Function() onTap;
const ConfirmVoidDialog(
{super.key,
required this.message,
required this.voidType,
required this.order,
required this.selectedItemQuantities,
required this.voidReason,
required this.onTap});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 8,
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: BoxConstraints(
maxWidth: 600,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
width: double.infinity,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[400]!, Colors.red[600]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_rounded,
color: Colors.white,
size: 28,
),
),
SizedBox(height: 12),
Text(
'Konfirmasi Void',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Tindakan ini tidak dapat dibatalkan',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
),
],
),
),
// Scrollable Content
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main message
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Text(
message,
style: TextStyle(
fontSize: 14,
height: 1.4,
color: Colors.grey[800],
),
),
),
if (voidType == 'item' &&
selectedItemQuantities.isNotEmpty) ...[
SizedBox(height: 16),
// Items section
Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.orange[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.orange[100],
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.list_alt_rounded,
color: Colors.orange[700],
size: 16,
),
),
SizedBox(width: 8),
Text(
'Item yang akan divoid:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.orange[800],
),
),
],
),
),
Container(
constraints: BoxConstraints(maxHeight: 120),
child: Scrollbar(
child: SingleChildScrollView(
padding: EdgeInsets.only(
left: 16, right: 16, bottom: 16),
child: Column(
children: selectedItemQuantities.entries
.map((entry) {
final item = order.orderItems!.firstWhere(
(item) => item.id == entry.key);
return Container(
margin: EdgeInsets.only(bottom: 6),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.03),
blurRadius: 2,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.red[400],
shape: BoxShape.circle,
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item.productName ??
'Unknown Product',
style: TextStyle(
fontWeight:
FontWeight.w600,
fontSize: 12,
),
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
Text(
'${entry.value} qty',
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red[100],
borderRadius:
BorderRadius.circular(4),
),
child: Text(
((item.unitPrice ?? 0) *
entry.value)
.currencyFormatRpV2,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.red[700],
),
),
),
],
),
);
}).toList(),
),
),
),
),
],
),
),
],
SizedBox(height: 16),
// Reason section
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.note_alt_rounded,
color: Colors.blue[700],
size: 16,
),
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alasan:',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue[800],
fontSize: 12,
),
),
SizedBox(height: 2),
Text(
voidReason,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.blue[700],
fontSize: 12,
),
),
],
),
),
],
),
),
],
),
),
),
// Action buttons
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16),
),
),
child: Row(
children: [
Expanded(
child: Container(
height: 44,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
foregroundColor: Colors.grey[700],
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.close_rounded, size: 18),
SizedBox(width: 6),
Text(
'Batal',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 44,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
onTap();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
foregroundColor: Colors.white,
elevation: 2,
shadowColor: Colors.red.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.delete_forever_rounded, size: 18),
SizedBox(width: 6),
Text(
voidType == 'all' ? 'Void Pesanan' : 'Void Item',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,814 @@
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';
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: () {
_showSuccessDialog();
},
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),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
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(
'Produk Pesanan (${widget.selectedOrder.orderItems?.length ?? 0})',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
SpaceHeight(16),
// Order Items List - Scrollable
...List.generate(
widget.selectedOrder.orderItems?.length ?? 0,
(index) {
final item = widget.selectedOrder.orderItems![index];
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),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
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;
}
void _processVoid() {
String confirmMessage;
if (voidType == 'all') {
confirmMessage =
'Apakah Anda yakin ingin membatalkan seluruh pesanan #${widget.selectedOrder.orderNumber}?\n\nIni 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\nJumlah 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 _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,
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'),
),
],
);
},
);
}
}

View File

@ -0,0 +1,293 @@
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:flutter/material.dart';
class VoidProductCard extends StatelessWidget {
final bool isSelected;
final OrderItem item;
final int voidQty;
final bool canSelect;
final Function()? onTapDecrease;
final Function()? onTapIncrease;
final Function()? onTapAll;
final Function()? onTapClear;
const VoidProductCard({
super.key,
required this.isSelected,
required this.item,
required this.voidQty,
required this.canSelect,
required this.onTapDecrease,
required this.onTapIncrease,
required this.onTapAll,
required this.onTapClear,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: isSelected ? AppColors.primary.withOpacity(0.1) : Colors.white,
border: Border.all(
color: isSelected ? AppColors.primary : Colors.grey[300]!,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
// Product Icon
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.fastfood,
color: AppColors.primary,
size: 24,
),
),
SizedBox(width: 12),
// Product Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName ?? 'Produk Tidak Diketahui',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
if (item.productVariantName != null)
Text(
'Varian: ${item.productVariantName}',
style:
TextStyle(fontSize: 12, color: Colors.grey[600]),
),
SizedBox(height: 4),
Row(
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Qty: ${item.quantity}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blue[700],
),
),
),
SizedBox(width: 8),
Text(
(item.unitPrice ?? 0).currencyFormatRpV2,
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
),
],
),
if (item.notes != null && item.notes!.isNotEmpty)
Padding(
padding: EdgeInsets.only(top: 4),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Catatan: ${item.notes}',
style: TextStyle(
fontSize: 11,
color: Colors.orange[700],
),
),
),
),
],
),
),
// Price
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
(item.totalPrice ?? 0).currencyFormatRpV2,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppColors.primary,
),
),
if (isSelected)
Container(
padding:
EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Void: ${((item.unitPrice ?? 0) * voidQty).currencyFormatRpV2}',
style: TextStyle(
fontSize: 12,
color: Colors.red[700],
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
// Quantity Controls (only show for item void)
if (canSelect) ...[
SizedBox(height: 16),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Row(
children: [
Text(
'Kuantitas:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
Spacer(),
// Quantity Controls
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Decrease Button
InkWell(
onTap: onTapDecrease,
child: Container(
padding: EdgeInsets.all(8),
child: Icon(
Icons.remove,
size: 16,
color: voidQty > 0
? AppColors.primary
: Colors.grey[400],
),
),
),
// Quantity Display
Container(
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: Colors.grey[300]!),
),
),
child: Text(
'$voidQty',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// Increase Button
InkWell(
onTap: onTapIncrease,
child: Container(
padding: EdgeInsets.all(8),
child: Icon(
Icons.add,
size: 16,
color: voidQty < (item.quantity ?? 0)
? AppColors.primary
: Colors.grey[400],
),
),
),
],
),
),
SizedBox(width: 12),
// Quick Actions
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: onTapAll,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
minimumSize: Size(0, 0),
),
child: Text(
'Semua',
style: TextStyle(
fontSize: 12,
color: AppColors.primary,
),
),
),
TextButton(
onPressed: onTapClear,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
minimumSize: Size(0, 0),
),
child: Text(
'Hapus',
style: TextStyle(
fontSize: 12,
color:
voidQty > 0 ? Colors.red : Colors.grey[400],
),
),
),
],
),
],
),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:flutter/material.dart';
class VoidLoading extends StatelessWidget {
const VoidLoading({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withOpacity(0.3),
child: Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
),
SizedBox(height: 16),
Text(
'Processing void...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:flutter/material.dart';
class VoidRadio extends StatelessWidget {
final String voidType;
final String value;
final Function(String?)? onChanged;
final String title;
final String subtitle;
const VoidRadio({
super.key,
required this.voidType,
required this.value,
this.onChanged,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: voidType == value ? AppColors.primary : Colors.grey[300]!,
width: voidType == value ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: RadioListTile<String>(
title: Text(
title,
style: TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
subtitle,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
value: value,
groupValue: voidType,
activeColor: AppColors.primary,
onChanged: onChanged),
);
}
}