diff --git a/lib/core/components/date_range_picker.dart b/lib/core/components/date_range_picker.dart new file mode 100644 index 0000000..8f8c52b --- /dev/null +++ b/lib/core/components/date_range_picker.dart @@ -0,0 +1,409 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datepicker/datepicker.dart'; + +class DateRangePickerModal { + static Future show({ + required BuildContext context, + String title = 'Pilih Rentang Tanggal', + DateTime? initialStartDate, + DateTime? initialEndDate, + DateTime? minDate, + DateTime? maxDate, + String confirmText = 'Pilih', + String cancelText = 'Batal', + Color primaryColor = Colors.blue, + Function(DateTime? startDate, DateTime? endDate)? onChanged, + }) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => _DateRangePickerDialog( + title: title, + initialStartDate: initialStartDate, + initialEndDate: initialEndDate, + minDate: minDate, + maxDate: maxDate, + confirmText: confirmText, + cancelText: cancelText, + primaryColor: primaryColor, + onChanged: onChanged, + ), + ); + } +} + +class _DateRangePickerDialog extends StatefulWidget { + final String title; + final DateTime? initialStartDate; + final DateTime? initialEndDate; + final DateTime? minDate; + final DateTime? maxDate; + final String confirmText; + final String cancelText; + final Color primaryColor; + final Function(DateTime? startDate, DateTime? endDate)? onChanged; + + const _DateRangePickerDialog({ + required this.title, + this.initialStartDate, + this.initialEndDate, + this.minDate, + this.maxDate, + required this.confirmText, + required this.cancelText, + required this.primaryColor, + this.onChanged, + }); + + @override + State<_DateRangePickerDialog> createState() => _DateRangePickerDialogState(); +} + +class _DateRangePickerDialogState extends State<_DateRangePickerDialog> + with TickerProviderStateMixin { + DateRangePickerSelectionChangedArgs? _selectionChangedArgs; + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 0.8, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.elasticOut, + )); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) { + setState(() { + _selectionChangedArgs = args; + }); + + // Note: onChanged callback is now called only when confirm button is pressed + // This allows users to see real-time selection without triggering callbacks + } + + String _getSelectionText() { + if (_selectionChangedArgs?.value is PickerDateRange) { + final PickerDateRange range = _selectionChangedArgs!.value; + if (range.startDate != null && range.endDate != null) { + return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}'; + } else if (range.startDate != null) { + return _formatDate(range.startDate!); + } + } + return 'Belum ada tanggal dipilih'; + } + + String _formatDate(DateTime date) { + final months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des' + ]; + return '${date.day} ${months[date.month - 1]} ${date.year}'; + } + + bool get _isValidSelection { + if (_selectionChangedArgs?.value is PickerDateRange) { + final PickerDateRange range = _selectionChangedArgs!.value; + return range.startDate != null && range.endDate != null; + } + return false; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + insetPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Container( + width: MediaQuery.of(context).size.width, + constraints: BoxConstraints( + maxWidth: 400, + maxHeight: MediaQuery.of(context).size.height * 0.85, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + widget.primaryColor, + widget.primaryColor.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today_rounded, + color: Colors.white, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + widget.title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + + // Scrollable Content + Flexible( + child: SingleChildScrollView( + child: Column( + children: [ + // Selection Info + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: widget.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: widget.primaryColor.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tanggal Terpilih:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: widget.primaryColor, + ), + ), + const SizedBox(height: 4), + Text( + _getSelectionText(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ), + + // Date Picker + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 320, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.withOpacity(0.2), + ), + ), + child: SfDateRangePicker( + onSelectionChanged: _onSelectionChanged, + selectionMode: + DateRangePickerSelectionMode.range, + initialSelectedRange: + (widget.initialStartDate != null && + widget.initialEndDate != null) + ? PickerDateRange( + widget.initialStartDate, + widget.initialEndDate, + ) + : null, + minDate: widget.minDate, + maxDate: widget.maxDate, + startRangeSelectionColor: widget.primaryColor, + endRangeSelectionColor: widget.primaryColor, + rangeSelectionColor: + widget.primaryColor.withOpacity(0.2), + todayHighlightColor: widget.primaryColor, + headerStyle: DateRangePickerHeaderStyle( + backgroundColor: Colors.transparent, + textAlign: TextAlign.center, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + monthViewSettings: + DateRangePickerMonthViewSettings( + viewHeaderStyle: + DateRangePickerViewHeaderStyle( + backgroundColor: + Colors.grey.withOpacity(0.1), + textStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: widget.primaryColor, + ), + ), + ), + selectionTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + rangeTextStyle: TextStyle( + color: widget.primaryColor, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ), + ], + ), + ), + ), + + // Action Buttons + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + style: OutlinedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 14), + side: BorderSide(color: Colors.grey.shade400), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + widget.cancelText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: _isValidSelection + ? () { + // Call onChanged when confirm button is pressed + if (widget.onChanged != null && + _selectionChangedArgs?.value + is PickerDateRange) { + final PickerDateRange range = + _selectionChangedArgs!.value; + widget.onChanged!( + range.startDate, range.endDate); + } + Navigator.of(context) + .pop(_selectionChangedArgs); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: widget.primaryColor, + padding: + const EdgeInsets.symmetric(vertical: 14), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + widget.confirmText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: _isValidSelection + ? Colors.white + : Colors.grey.shade600, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/report/pages/report_page.dart b/lib/presentation/report/pages/report_page.dart index c052804..879a125 100644 --- a/lib/presentation/report/pages/report_page.dart +++ b/lib/presentation/report/pages/report_page.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:enaklo_pos/core/components/date_range_picker.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/core/utils/helper_pdf_service.dart'; import 'package:enaklo_pos/core/utils/permession_handler.dart'; @@ -12,7 +13,6 @@ import 'package:enaklo_pos/presentation/report/widgets/inventory_report_widget.d import 'package:enaklo_pos/presentation/report/widgets/profit_loss_widget.dart'; import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:enaklo_pos/core/components/custom_date_picker.dart'; import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; import 'package:enaklo_pos/core/utils/date_formatter.dart'; @@ -55,60 +55,99 @@ class _ReportPageState extends State { ); } + onDateChanged(DateTime? startDate, DateTime? endDate) { + setState(() { + fromDate = startDate ?? fromDate; + toDate = endDate ?? toDate; + }); + context.read().add( + ReportEvent.get(startDate: fromDate, endDate: toDate), + ); + if (selectedMenu == 0) { + context.read().add( + SummaryEvent.getSummary(fromDate, toDate), + ); + } + + if (selectedMenu == 2) { + context.read().add( + ItemSalesReportEvent.getItemSales( + startDate: fromDate, endDate: toDate), + ); + } + + if (selectedMenu == 3) { + context.read().add( + ProductSalesEvent.getProductSales( + fromDate, + toDate, + ), + ); + } + + if (selectedMenu == 4) { + context.read().add( + PaymentMethodReportEvent.getPaymentMethodReport( + startDate: fromDate, + endDate: toDate, + ), + ); + } + + if (selectedMenu == 5) { + context.read().add( + ProfitLossEvent.getProfitLoss( + fromDate, + toDate, + ), + ); + } + + if (selectedMenu == 6) { + context.read().add( + InventoryReportEvent.get( + startDate: fromDate, + endDate: toDate, + ), + ); + } + } + @override Widget build(BuildContext context) { String searchDateFormatted = - '${fromDate.toFormattedDate2()} to ${toDate.toFormattedDate2()}'; + '${fromDate.toFormattedDate2()} - ${toDate.toFormattedDate2()}'; return Scaffold( backgroundColor: AppColors.background, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ReportTitle( + searchDateFormatted: searchDateFormatted, actionWidget: [ - SizedBox( - width: 300, - child: CustomDatePicker( - prefix: const Text('From: '), - initialDate: fromDate, - onDateSelected: (selectedDate) { - fromDate = selectedDate; - context.read().add( - ReportEvent.get(startDate: fromDate, endDate: toDate), - ); - setState(() {}); - }, - ), - ), - const SpaceWidth(12.0), - SizedBox( - width: 300, - child: CustomDatePicker( - prefix: const Text('To: '), - initialDate: toDate, - onDateSelected: (selectedDate) { - toDate = selectedDate; - context.read().add( - ReportEvent.get(startDate: fromDate, endDate: toDate), - ); - setState(() {}); - // context.read().add( - // TransactionReportEvent.getReport( - // startDate: - // DateFormatter.formatDateTime( - // fromDate), - // endDate: DateFormatter.formatDateTime( - // toDate)), - // ); - // context.read().add( - // ItemSalesReportEvent.getItemSales( - // startDate: - // DateFormatter.formatDateTime( - // fromDate), - // endDate: DateFormatter.formatDateTime( - // toDate)), - // ); - }, + InkWell( + onTap: () { + DateRangePickerModal.show( + context: context, + initialEndDate: toDate, + initialStartDate: fromDate, + primaryColor: AppColors.primary, + onChanged: onDateChanged, + ); + }, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + border: Border.all( + color: AppColors.stroke, + )), + child: Icon( + Icons.calendar_month_outlined, + color: AppColors.primary, + size: 28, + ), ), ), const SpaceWidth(12.0), diff --git a/lib/presentation/report/widgets/report_title.dart b/lib/presentation/report/widgets/report_title.dart index ea5ef72..12e3fff 100644 --- a/lib/presentation/report/widgets/report_title.dart +++ b/lib/presentation/report/widgets/report_title.dart @@ -1,12 +1,13 @@ import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; -import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; import '../../../core/constants/colors.dart'; class ReportTitle extends StatelessWidget { final List? actionWidget; - const ReportTitle({super.key, this.actionWidget}); + final String searchDateFormatted; + const ReportTitle( + {super.key, this.actionWidget, required this.searchDateFormatted}); @override Widget build(BuildContext context) { @@ -41,7 +42,7 @@ class ReportTitle extends StatelessWidget { ), ), Text( - DateTime.now().toFormattedDate2(), + searchDateFormatted, style: TextStyle( color: AppColors.grey, fontSize: 14, diff --git a/lib/presentation/sales/pages/sales_page.dart b/lib/presentation/sales/pages/sales_page.dart index 5198f4a..15a89ff 100644 --- a/lib/presentation/sales/pages/sales_page.dart +++ b/lib/presentation/sales/pages/sales_page.dart @@ -98,8 +98,8 @@ class _SalesPageState extends State { }, onDateRangeChanged: (start, end) { setState(() { - startDate = start; - endDate = end; + startDate = start ?? startDate; + endDate = end ?? endDate; }); context.read().add( diff --git a/lib/presentation/sales/widgets/sales_title.dart b/lib/presentation/sales/widgets/sales_title.dart index 0700828..f0fbc4a 100644 --- a/lib/presentation/sales/widgets/sales_title.dart +++ b/lib/presentation/sales/widgets/sales_title.dart @@ -1,9 +1,9 @@ import 'package:enaklo_pos/core/components/components.dart'; +import 'package:enaklo_pos/core/components/date_range_picker.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/date_time_ext.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; -import 'package:enaklo_pos/presentation/sales/dialog/filter_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +12,7 @@ class SalesTitle extends StatelessWidget { final DateTime startDate; final DateTime endDate; final Function(String) onChanged; - final void Function(DateTime start, DateTime end) onDateRangeChanged; + final void Function(DateTime? start, DateTime? end) onDateRangeChanged; const SalesTitle( {super.key, @@ -119,13 +119,12 @@ class SalesTitle extends StatelessWidget { ), SpaceWidth(12), GestureDetector( - onTap: () => showDialog( + onTap: () => DateRangePickerModal.show( context: context, - builder: (context) => SalesFilterDialog( - startDate: startDate, - endDate: endDate, - onDateRangeChanged: onDateRangeChanged, - ), + initialStartDate: startDate, + initialEndDate: endDate, + primaryColor: AppColors.primary, + onChanged: onDateRangeChanged, ), child: Container( padding: const EdgeInsets.all(12), diff --git a/pubspec.lock b/pubspec.lock index 3efa4dc..2c6e2ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1386,6 +1386,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: ce02ce65f51db8e29edc9d2225872d927e001bd2b13c2490d176563bbb046fc7 + url: "https://pub.dev" + source: hosted + version: "30.2.5" + syncfusion_flutter_datepicker: + dependency: "direct main" + description: + name: syncfusion_flutter_datepicker + sha256: e8df9f4777df15db11929f20cbe98e4249fe08208e7107bcb4ad889aa1ba2bbf + url: "https://pub.dev" + source: hosted + version: "30.2.5" synchronized: dependency: transitive description: @@ -1571,5 +1587,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4f4e6cc..a71c97d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: fl_chart: ^1.0.0 barcode: ^2.2.9 barcode_image: ^2.0.3 + syncfusion_flutter_datepicker: ^30.2.5 # imin_printer: ^0.6.10 dev_dependencies: