This commit is contained in:
efrilm
2025-10-31 14:33:02 +07:00
parent bf43c398d2
commit 8d9e622121
24 changed files with 3054 additions and 79 deletions
@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/void/void_form/void_form_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/types/void_type.dart';
import '../../../components/spaces/space.dart';
import 'void_product_card.dart';
class VoidLeftPanel extends StatefulWidget {
final VoidFormState state;
const VoidLeftPanel({super.key, required this.state});
@override
State<VoidLeftPanel> createState() => _VoidLeftPanelState();
}
class _VoidLeftPanelState extends State<VoidLeftPanel> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: AppColor.border),
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Icon(Icons.receipt_long, color: AppColor.primary, size: 24),
SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pesanan #${widget.state.order.orderNumber}',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Text(
'Meja: ${widget.state.order.tableNumber}${widget.state.order.orderType}',
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor(
widget.state.order.status,
).withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text(
widget.state.order.status.toUpperCase(),
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
color: _getStatusColor(widget.state.order.status),
),
),
),
],
),
),
Expanded(
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.shopping_cart,
color: AppColor.primary,
size: 20,
),
SpaceWidth(8),
Text(
'Produk Pesanan (${widget.state.pendingItems.length})',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
],
),
SpaceHeight(16),
...List.generate(widget.state.pendingItems.length, (index) {
final item = widget.state.pendingItems[index];
final voidQty =
widget.state.selectedItemQuantities[item.id] ?? 0;
final isSelected = voidQty > 0;
final canSelect = widget.state.voidType.isItem;
return VoidProductCard(
isSelected: isSelected,
item: item,
voidQty: voidQty,
canSelect: canSelect,
onTapDecrease: voidQty > 0
? () {
context.read<VoidFormBloc>().add(
VoidFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: voidQty - 1,
),
);
}
: null,
onTapIncrease: voidQty < item.quantity
? () {
context.read<VoidFormBloc>().add(
VoidFormEvent.itemQuantityChanged(
itemId: item.id,
quantity: voidQty + 1,
),
);
}
: null,
);
}),
],
),
),
),
),
Container(
padding: EdgeInsets.all(16),
child: Column(
children: [
_buildSummaryRow(
'Subtotal:',
(widget.state.order.subtotal).currencyFormatRpV2,
),
_buildSummaryRow(
'Pajak:',
(widget.state.order.taxAmount).currencyFormatRpV2,
),
// _buildSummaryRow(
// 'Diskon:',
// '- ${(widget.state.order.discountAmount).currencyFormatRpV2}',
// ),
Divider(thickness: 1),
_buildSummaryRow(
'Total:',
(widget.state.order.totalAmount).currencyFormatRpV2,
isTotal: true,
),
if (widget.state.voidType.isItem &&
widget.state.selectedItemQuantities.isNotEmpty) ...[
Divider(thickness: 1, color: Colors.red[300]),
_buildSummaryRow(
'Total Void:',
'- ${widget.state.totalPriceVoid.currencyFormatRpV2}',
isVoid: true,
),
],
],
),
),
],
),
);
}
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
? AppColor.error
: (isTotal ? AppColor.primary : AppColor.textPrimary),
),
),
Text(
value,
style: TextStyle(
fontSize: isTotal ? 16 : 14,
fontWeight: isTotal ? FontWeight.bold : FontWeight.w500,
color: isVoid
? AppColor.error
: (isTotal ? AppColor.primary : AppColor.textPrimary),
),
),
],
),
);
}
Color _getStatusColor(String? status) {
switch (status?.toLowerCase()) {
case 'completed':
return AppColor.success;
case 'pending':
return AppColor.warning;
case 'cancelled':
return AppColor.error;
case 'processing':
return AppColor.info;
default:
return AppColor.primary;
}
}
}
@@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/order/order.dart';
import '../../../components/spaces/space.dart';
class VoidProductCard extends StatelessWidget {
final bool isSelected;
final OrderItem item;
final int voidQty;
final bool canSelect;
final Function()? onTapDecrease;
final Function()? onTapIncrease;
const VoidProductCard({
super.key,
required this.isSelected,
required this.item,
required this.voidQty,
required this.canSelect,
required this.onTapDecrease,
required this.onTapIncrease,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: isSelected ? AppColor.primary.withOpacity(0.1) : Colors.white,
border: Border.all(
color: isSelected ? AppColor.primary : AppColor.border,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
// Product Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.w600,
),
),
if (item.productVariantName.isNotEmpty)
Text(
'Varian: ${item.productVariantName}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
SpaceHeight(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: AppStyle.sm.copyWith(
fontWeight: FontWeight.w500,
color: Colors.blue[700],
),
),
),
SpaceWidth(8),
Text(
(item.unitPrice).currencyFormatRpV2,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
if (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: AppStyle.xs.copyWith(
color: AppColor.warning,
),
),
),
),
],
),
),
if (canSelect) ...[
Container(
decoration: BoxDecoration(
border: Border.all(color: AppColor.border),
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
? AppColor.primary
: Colors.grey[400],
),
),
),
// Quantity Display
Container(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: AppColor.border),
),
),
child: Text(
'$voidQty',
style: AppStyle.sm.copyWith(
fontWeight: FontWeight.w600,
),
),
),
// Increase Button
InkWell(
onTap: onTapIncrease,
child: Container(
padding: EdgeInsets.all(8),
child: Icon(
Icons.add,
size: 16,
color: voidQty < (item.quantity)
? AppColor.primary
: Colors.grey[400],
),
),
),
],
),
),
SpaceWidth(20),
],
// Price
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
(item.totalPrice).currencyFormatRpV2,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.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) * voidQty).currencyFormatRpV2}',
style: AppStyle.sm.copyWith(
fontSize: 12,
color: AppColor.error,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
// Quantity Controls (only show for item void)
],
),
),
);
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/types/void_type.dart';
class VoidRadio extends StatelessWidget {
final VoidType voidType;
final VoidType value;
final Function(VoidType?)? 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 ? AppColor.primary : Colors.grey[300]!,
width: voidType == value ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: RadioListTile<VoidType>(
title: Text(
title,
style: AppStyle.md.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
subtitle,
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
value: value,
groupValue: voidType,
activeColor: AppColor.primary,
onChanged: onChanged,
),
);
}
}
@@ -0,0 +1,372 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/void/void_form/void_form_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/types/void_type.dart';
import '../../../components/button/button.dart';
import '../../../components/dialog/order/order_void_confirm_dialog.dart';
import '../../../components/field/field.dart';
import '../../../components/spaces/space.dart';
import 'void_radio.dart';
class VoidRightPanel extends StatefulWidget {
final VoidFormState state;
const VoidRightPanel({super.key, required this.state});
@override
State<VoidRightPanel> createState() => _VoidRightPanelState();
}
class _VoidRightPanelState extends State<VoidRightPanel> {
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: AppColor.border),
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Text(
'Konfigurasi Void',
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
],
),
),
Expanded(
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tipe Void *',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
SpaceHeight(12),
VoidRadio(
voidType: widget.state.voidType,
value: VoidType.all,
title: 'Batalkan Seluruh Pesanan',
subtitle: "Batalkan pesanan lengkap dan semua item",
onChanged: (VoidType? value) {
context.read<VoidFormBloc>().add(
VoidFormEvent.clearSelectedItem(),
);
context.read<VoidFormBloc>().add(
VoidFormEvent.voidTypeChanged(value!),
);
},
),
SpaceHeight(12),
VoidRadio(
voidType: widget.state.voidType,
value: VoidType.item,
title: 'Batalkan Barang/Jumlah Tertentu',
subtitle:
"Mengurangi atau membatalkan jumlah item tertentu",
onChanged: (VoidType? value) {
context.read<VoidFormBloc>().add(
VoidFormEvent.voidTypeChanged(value!),
);
},
),
SpaceHeight(24),
// Selected Items Summary (only show for item void)
if (widget.state.voidType.isItem) ...[
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.state.selectedItemQuantities.isEmpty
? AppColor.warning.withOpacity(0.1)
: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.state.selectedItemQuantities.isEmpty
? AppColor.warning.withOpacity(0.6)
: AppColor.success.withOpacity(0.6),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
widget.state.selectedItemQuantities.isEmpty
? Icons.warning
: Icons.check_circle,
color:
widget
.state
.selectedItemQuantities
.isEmpty
? AppColor.warning
: AppColor.success,
size: 20,
),
SpaceWidth(8),
Expanded(
child: Text(
widget.state.selectedItemQuantities.isEmpty
? 'Silakan pilih item dan jumlah yang akan dibatalkan'
: 'Item yang dipilih untuk dibatalkan:',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.w500,
color:
widget
.state
.selectedItemQuantities
.isEmpty
? AppColor.warning
: AppColor.success,
),
),
),
],
),
if (widget
.state
.selectedItemQuantities
.isNotEmpty) ...[
SpaceHeight(12),
Container(
constraints: BoxConstraints(maxHeight: 150),
child: Scrollbar(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: widget
.state
.selectedItemQuantities
.entries
.map((entry) {
final item = widget
.state
.order
.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: AppColor.success,
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
item.productName,
style: AppStyle.sm
.copyWith(
fontWeight:
FontWeight
.w500,
),
),
Text(
'Qty: ${entry.value}',
style: AppStyle.xs
.copyWith(
color: AppColor
.textSecondary,
),
),
],
),
),
Text(
((item.unitPrice) *
entry.value)
.currencyFormatRpV2,
style: AppStyle.xs.copyWith(
fontWeight:
FontWeight.bold,
color: AppColor.error,
),
),
],
),
);
})
.toList(),
),
),
),
),
Divider(height: 16, thickness: 1),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.error,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'Jumlah Total Void:',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
widget
.state
.totalPriceVoid
.currencyFormatRpV2,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
],
),
),
SpaceHeight(24),
],
Text(
'Alasan Void *',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
SpaceHeight(8),
AppTextFormField(
label: 'Harap berikan alasan untuk membatalkan...',
showLabel: false,
maxLines: 4,
onChanged: (value) {
context.read<VoidFormBloc>().add(
VoidFormEvent.voidReasonChanged(value),
);
},
),
SpaceHeight(32),
],
),
),
),
),
// Action Buttons
Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
Expanded(
child: AppElevatedButton.outlined(
onPressed: () => context.router.maybePop(),
label: 'Batal',
),
),
SpaceWidth(12),
Expanded(
child: AppElevatedButton.filled(
onPressed: _canProcessVoid() ? _processVoid : null,
label: widget.state.voidType.isAll
? 'Void Pesanan'
: 'Void Produk',
),
),
],
),
),
],
),
);
}
bool _canProcessVoid() {
if (widget.state.voidReason?.isEmpty ?? true) return false;
if (widget.state.voidType.isItem &&
widget.state.selectedItemQuantities.isEmpty) {
return false;
}
return true;
}
void _processVoid() {
String confirmMessage;
if (widget.state.voidType.isAll) {
confirmMessage =
'Apakah Anda yakin ingin membatalkan seluruh pesanan #${widget.state.order.orderNumber}?\n\nIni akan membatalkan semua item dalam pesanan.';
} else {
int totalItems = widget.state.selectedItemQuantities.values.fold(
0,
(sum, qty) => sum + qty,
);
confirmMessage =
'Apakah Anda yakin ingin membatalkan $totalItems item dari pesanan #${widget.state.order.orderNumber}?\n\nJumlah Batal: ${widget.state.totalPriceVoid.currencyFormatRpV2}';
}
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return OrderVoidConfirmDialog(message: confirmMessage);
},
);
}
}