From 1f6e5e9a2ba5575fdffefbbe7da5bc3bfd8a9bf6 Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 18 Aug 2025 16:43:07 +0700 Subject: [PATCH] feat: sales range date --- .../sales_loader/sales_loader_bloc.dart | 31 +- .../sales_loader_bloc.freezed.dart | 227 +++++++- .../sales_loader/sales_loader_event.dart | 4 + .../sales_loader/sales_loader_state.dart | 4 + lib/injection.config.dart | 28 +- .../bottom_sheet/date_range_bottom_sheet.dart | 363 ++++++++++++ .../field/date_range_picker_field.dart | 531 ++++++++++++++++++ lib/presentation/pages/sales/sales_page.dart | 269 ++++----- pubspec.lock | 18 +- pubspec.yaml | 1 + 10 files changed, 1286 insertions(+), 190 deletions(-) create mode 100644 lib/presentation/components/bottom_sheet/date_range_bottom_sheet.dart create mode 100644 lib/presentation/components/field/date_range_picker_field.dart diff --git a/lib/application/analytic/sales_loader/sales_loader_bloc.dart b/lib/application/analytic/sales_loader/sales_loader_bloc.dart index 453002b..e30dde8 100644 --- a/lib/application/analytic/sales_loader/sales_loader_bloc.dart +++ b/lib/application/analytic/sales_loader/sales_loader_bloc.dart @@ -21,19 +21,26 @@ class SalesLoaderBloc extends Bloc { Future _onSalesLoaderEvent( SalesLoaderEvent event, Emitter emit, - ) async { - emit(state.copyWith(isFetching: true, failureOptionSales: none())); + ) { + return event.map( + rangeDateChanged: (e) async { + emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo)); + }, + fectched: (e) async { + emit(state.copyWith(isFetching: true, failureOptionSales: none())); - final result = await _analyticRepository.getSales( - dateFrom: DateTime.now().subtract(const Duration(days: 30)), - dateTo: DateTime.now(), + final result = await _analyticRepository.getSales( + dateFrom: state.dateFrom, + dateTo: state.dateTo, + ); + + var data = result.fold( + (f) => state.copyWith(failureOptionSales: optionOf(f)), + (sales) => state.copyWith(sales: sales), + ); + + emit(data.copyWith(isFetching: false)); + }, ); - - var data = result.fold( - (f) => state.copyWith(failureOptionSales: optionOf(f)), - (sales) => state.copyWith(sales: sales), - ); - - emit(data.copyWith(isFetching: false)); } } diff --git a/lib/application/analytic/sales_loader/sales_loader_bloc.freezed.dart b/lib/application/analytic/sales_loader/sales_loader_bloc.freezed.dart index 0f7b470..e0ce519 100644 --- a/lib/application/analytic/sales_loader/sales_loader_bloc.freezed.dart +++ b/lib/application/analytic/sales_loader/sales_loader_bloc.freezed.dart @@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$SalesLoaderEvent { @optionalTypeArgs TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, required TResult Function() fectched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult? Function()? fectched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult Function()? fectched, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, required TResult Function(_Fectched value) fectched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, TResult? Function(_Fectched value)? fectched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, TResult Function(_Fectched value)? fectched, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -67,6 +74,164 @@ class _$SalesLoaderEventCopyWithImpl<$Res, $Val extends SalesLoaderEvent> /// with the given fields replaced by the non-null parameter values. } +/// @nodoc +abstract class _$$RangeDateChangedImplCopyWith<$Res> { + factory _$$RangeDateChangedImplCopyWith( + _$RangeDateChangedImpl value, + $Res Function(_$RangeDateChangedImpl) then, + ) = __$$RangeDateChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({DateTime dateFrom, DateTime dateTo}); +} + +/// @nodoc +class __$$RangeDateChangedImplCopyWithImpl<$Res> + extends _$SalesLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl> + implements _$$RangeDateChangedImplCopyWith<$Res> { + __$$RangeDateChangedImplCopyWithImpl( + _$RangeDateChangedImpl _value, + $Res Function(_$RangeDateChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SalesLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? dateFrom = null, Object? dateTo = null}) { + return _then( + _$RangeDateChangedImpl( + null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} + +/// @nodoc + +class _$RangeDateChangedImpl implements _RangeDateChanged { + const _$RangeDateChangedImpl(this.dateFrom, this.dateTo); + + @override + final DateTime dateFrom; + @override + final DateTime dateTo; + + @override + String toString() { + return 'SalesLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RangeDateChangedImpl && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); + } + + @override + int get hashCode => Object.hash(runtimeType, dateFrom, dateTo); + + /// Create a copy of SalesLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith => + __$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, + required TResult Function() fectched, + }) { + return rangeDateChanged(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult? Function()? fectched, + }) { + return rangeDateChanged?.call(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult Function()? fectched, + required TResult orElse(), + }) { + if (rangeDateChanged != null) { + return rangeDateChanged(dateFrom, dateTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, + required TResult Function(_Fectched value) fectched, + }) { + return rangeDateChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, + TResult? Function(_Fectched value)? fectched, + }) { + return rangeDateChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, + TResult Function(_Fectched value)? fectched, + required TResult orElse(), + }) { + if (rangeDateChanged != null) { + return rangeDateChanged(this); + } + return orElse(); + } +} + +abstract class _RangeDateChanged implements SalesLoaderEvent { + const factory _RangeDateChanged( + final DateTime dateFrom, + final DateTime dateTo, + ) = _$RangeDateChangedImpl; + + DateTime get dateFrom; + DateTime get dateTo; + + /// Create a copy of SalesLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc abstract class _$$FectchedImplCopyWith<$Res> { factory _$$FectchedImplCopyWith( @@ -110,6 +275,8 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, required TResult Function() fectched, }) { return fectched(); @@ -118,6 +285,7 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult? Function()? fectched, }) { return fectched?.call(); @@ -126,6 +294,7 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult Function()? fectched, required TResult orElse(), }) { @@ -138,6 +307,7 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, required TResult Function(_Fectched value) fectched, }) { return fectched(this); @@ -146,6 +316,7 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, TResult? Function(_Fectched value)? fectched, }) { return fectched?.call(this); @@ -154,6 +325,7 @@ class _$FectchedImpl implements _Fectched { @override @optionalTypeArgs TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, TResult Function(_Fectched value)? fectched, required TResult orElse(), }) { @@ -174,6 +346,8 @@ mixin _$SalesLoaderState { Option get failureOptionSales => throw _privateConstructorUsedError; bool get isFetching => throw _privateConstructorUsedError; + DateTime get dateFrom => throw _privateConstructorUsedError; + DateTime get dateTo => throw _privateConstructorUsedError; /// Create a copy of SalesLoaderState /// with the given fields replaced by the non-null parameter values. @@ -193,6 +367,8 @@ abstract class $SalesLoaderStateCopyWith<$Res> { SalesAnalytic sales, Option failureOptionSales, bool isFetching, + DateTime dateFrom, + DateTime dateTo, }); $SalesAnalyticCopyWith<$Res> get sales; @@ -216,6 +392,8 @@ class _$SalesLoaderStateCopyWithImpl<$Res, $Val extends SalesLoaderState> Object? sales = null, Object? failureOptionSales = null, Object? isFetching = null, + Object? dateFrom = null, + Object? dateTo = null, }) { return _then( _value.copyWith( @@ -231,6 +409,14 @@ class _$SalesLoaderStateCopyWithImpl<$Res, $Val extends SalesLoaderState> ? _value.isFetching : isFetching // ignore: cast_nullable_to_non_nullable as bool, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, ) as $Val, ); @@ -260,6 +446,8 @@ abstract class _$$SalesLoaderStateImplCopyWith<$Res> SalesAnalytic sales, Option failureOptionSales, bool isFetching, + DateTime dateFrom, + DateTime dateTo, }); @override @@ -283,6 +471,8 @@ class __$$SalesLoaderStateImplCopyWithImpl<$Res> Object? sales = null, Object? failureOptionSales = null, Object? isFetching = null, + Object? dateFrom = null, + Object? dateTo = null, }) { return _then( _$SalesLoaderStateImpl( @@ -298,6 +488,14 @@ class __$$SalesLoaderStateImplCopyWithImpl<$Res> ? _value.isFetching : isFetching // ignore: cast_nullable_to_non_nullable as bool, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, ), ); } @@ -310,6 +508,8 @@ class _$SalesLoaderStateImpl implements _SalesLoaderState { required this.sales, required this.failureOptionSales, this.isFetching = false, + required this.dateFrom, + required this.dateTo, }); @override @@ -319,10 +519,14 @@ class _$SalesLoaderStateImpl implements _SalesLoaderState { @override @JsonKey() final bool isFetching; + @override + final DateTime dateFrom; + @override + final DateTime dateTo; @override String toString() { - return 'SalesLoaderState(sales: $sales, failureOptionSales: $failureOptionSales, isFetching: $isFetching)'; + return 'SalesLoaderState(sales: $sales, failureOptionSales: $failureOptionSales, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)'; } @override @@ -334,12 +538,21 @@ class _$SalesLoaderStateImpl implements _SalesLoaderState { (identical(other.failureOptionSales, failureOptionSales) || other.failureOptionSales == failureOptionSales) && (identical(other.isFetching, isFetching) || - other.isFetching == isFetching)); + other.isFetching == isFetching) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); } @override - int get hashCode => - Object.hash(runtimeType, sales, failureOptionSales, isFetching); + int get hashCode => Object.hash( + runtimeType, + sales, + failureOptionSales, + isFetching, + dateFrom, + dateTo, + ); /// Create a copy of SalesLoaderState /// with the given fields replaced by the non-null parameter values. @@ -358,6 +571,8 @@ abstract class _SalesLoaderState implements SalesLoaderState { required final SalesAnalytic sales, required final Option failureOptionSales, final bool isFetching, + required final DateTime dateFrom, + required final DateTime dateTo, }) = _$SalesLoaderStateImpl; @override @@ -366,6 +581,10 @@ abstract class _SalesLoaderState implements SalesLoaderState { Option get failureOptionSales; @override bool get isFetching; + @override + DateTime get dateFrom; + @override + DateTime get dateTo; /// Create a copy of SalesLoaderState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/application/analytic/sales_loader/sales_loader_event.dart b/lib/application/analytic/sales_loader/sales_loader_event.dart index 4838c2e..a70390b 100644 --- a/lib/application/analytic/sales_loader/sales_loader_event.dart +++ b/lib/application/analytic/sales_loader/sales_loader_event.dart @@ -2,5 +2,9 @@ part of 'sales_loader_bloc.dart'; @freezed class SalesLoaderEvent with _$SalesLoaderEvent { + const factory SalesLoaderEvent.rangeDateChanged( + DateTime dateFrom, + DateTime dateTo, + ) = _RangeDateChanged; const factory SalesLoaderEvent.fectched() = _Fectched; } diff --git a/lib/application/analytic/sales_loader/sales_loader_state.dart b/lib/application/analytic/sales_loader/sales_loader_state.dart index 79cea6f..7cbe4f3 100644 --- a/lib/application/analytic/sales_loader/sales_loader_state.dart +++ b/lib/application/analytic/sales_loader/sales_loader_state.dart @@ -6,10 +6,14 @@ class SalesLoaderState with _$SalesLoaderState { required SalesAnalytic sales, required Option failureOptionSales, @Default(false) bool isFetching, + required DateTime dateFrom, + required DateTime dateTo, }) = _SalesLoaderState; factory SalesLoaderState.initial() => SalesLoaderState( sales: SalesAnalytic.empty(), failureOptionSales: none(), + dateFrom: DateTime.now().subtract(const Duration(days: 30)), + dateTo: DateTime.now(), ); } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 78b31ec..5864f04 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -130,6 +130,9 @@ extension GetItInjectableX on _i174.GetIt { () => _i115.ApiClient(gh<_i361.Dio>(), gh<_i6.Env>()), ); gh.factory<_i6.Env>(() => _i6.ProdEnv(), registerFor: {_prod}); + gh.factory<_i130.OrderRemoteDataProvider>( + () => _i130.OrderRemoteDataProvider(gh<_i115.ApiClient>()), + ); gh.factory<_i333.CategoryRemoteDataProvider>( () => _i333.CategoryRemoteDataProvider(gh<_i115.ApiClient>()), ); @@ -145,9 +148,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i1006.CustomerRemoteDataProvider>( () => _i1006.CustomerRemoteDataProvider(gh<_i115.ApiClient>()), ); - gh.factory<_i130.OrderRemoteDataProvider>( - () => _i130.OrderRemoteDataProvider(gh<_i115.ApiClient>()), - ); gh.factory<_i48.ICustomerRepository>( () => _i550.CustomerRepository(gh<_i1006.CustomerRemoteDataProvider>()), ); @@ -184,26 +184,26 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i889.SalesLoaderBloc>( () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), ); - gh.factory<_i11.ProfitLossLoaderBloc>( - () => _i11.ProfitLossLoaderBloc(gh<_i477.IAnalyticRepository>()), - ); - gh.factory<_i1038.CategoryAnalyticLoaderBloc>( - () => _i1038.CategoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + gh.factory<_i221.ProductAnalyticLoaderBloc>( + () => _i221.ProductAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), ); gh.factory<_i785.InventoryAnalyticLoaderBloc>( () => _i785.InventoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), ); - gh.factory<_i516.DashboardAnalyticLoaderBloc>( - () => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), - ); - gh.factory<_i221.ProductAnalyticLoaderBloc>( - () => _i221.ProductAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), - ); gh.factory<_i552.PaymentMethodAnalyticLoaderBloc>( () => _i552.PaymentMethodAnalyticLoaderBloc( gh<_i477.IAnalyticRepository>(), ), ); + gh.factory<_i1038.CategoryAnalyticLoaderBloc>( + () => _i1038.CategoryAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + ); + gh.factory<_i11.ProfitLossLoaderBloc>( + () => _i11.ProfitLossLoaderBloc(gh<_i477.IAnalyticRepository>()), + ); + gh.factory<_i516.DashboardAnalyticLoaderBloc>( + () => _i516.DashboardAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()), + ); gh.factory<_i775.LoginFormBloc>( () => _i775.LoginFormBloc(gh<_i49.IAuthRepository>()), ); diff --git a/lib/presentation/components/bottom_sheet/date_range_bottom_sheet.dart b/lib/presentation/components/bottom_sheet/date_range_bottom_sheet.dart new file mode 100644 index 0000000..cb22eae --- /dev/null +++ b/lib/presentation/components/bottom_sheet/date_range_bottom_sheet.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datepicker/datepicker.dart'; + +class DateRangePickerBottomSheet { + 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 showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: false, + enableDrag: false, + builder: (BuildContext context) => _DateRangePickerBottomSheet( + title: title, + initialStartDate: initialStartDate, + initialEndDate: initialEndDate, + minDate: minDate, + maxDate: maxDate, + confirmText: confirmText, + cancelText: cancelText, + primaryColor: primaryColor, + onChanged: onChanged, + ), + ); + } +} + +class _DateRangePickerBottomSheet 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 _DateRangePickerBottomSheet({ + required this.title, + this.initialStartDate, + this.initialEndDate, + this.minDate, + this.maxDate, + required this.confirmText, + required this.cancelText, + required this.primaryColor, + this.onChanged, + }); + + @override + State<_DateRangePickerBottomSheet> createState() => + _DateRangePickerBottomSheetState(); +} + +class _DateRangePickerBottomSheetState + extends State<_DateRangePickerBottomSheet> + with TickerProviderStateMixin { + DateRangePickerSelectionChangedArgs? _selectionChangedArgs; + late AnimationController _animationController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _slideAnimation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) { + setState(() { + _selectionChangedArgs = args; + }); + } + + 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) { + final screenHeight = MediaQuery.of(context).size.height; + final bottomSheetHeight = screenHeight * 0.75; + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, _slideAnimation.value * bottomSheetHeight), + child: Container( + height: bottomSheetHeight, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag Handle + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Selection Info + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: widget.primaryColor.withOpacity(0.08), + 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: 6), + Text( + _getSelectionText(), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Date Picker + 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, + ), + ), + ), + ], + ), + ), + ), + + // Bottom Fixed Action Buttons + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + ), + child: SafeArea( + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: Colors.grey.shade400), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + widget.cancelText, + style: const TextStyle( + fontSize: 15, + 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: 16), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey.shade300, + ), + child: Text( + widget.confirmText, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: _isValidSelection + ? Colors.white + : Colors.grey.shade600, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/components/field/date_range_picker_field.dart b/lib/presentation/components/field/date_range_picker_field.dart new file mode 100644 index 0000000..bf495f9 --- /dev/null +++ b/lib/presentation/components/field/date_range_picker_field.dart @@ -0,0 +1,531 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datepicker/datepicker.dart'; + +import '../../../common/theme/theme.dart'; +import '../bottom_sheet/date_range_bottom_sheet.dart'; + +class DateRangePickerField extends StatefulWidget { + final String? label; + final String placeholder; + final DateTime? startDate; + final DateTime? endDate; + final DateTime? minDate; + final DateTime? maxDate; + final Function(DateTime? startDate, DateTime? endDate)? onChanged; + final Color primaryColor; + final bool enabled; + final String? errorText; + final EdgeInsetsGeometry? padding; + final TextStyle? textStyle; + final TextStyle? placeholderStyle; + final BoxDecoration? decoration; + final double height; + + const DateRangePickerField({ + Key? key, + this.label, + this.placeholder = 'Pilih rentang tanggal', + this.startDate, + this.endDate, + this.minDate, + this.maxDate, + this.onChanged, + this.primaryColor = AppColor.primary, + this.enabled = true, + this.errorText, + this.padding, + this.textStyle, + this.placeholderStyle, + this.decoration, + this.height = 52.0, + }) : super(key: key); + + @override + State createState() => _DateRangePickerFieldState(); +} + +class _DateRangePickerFieldState extends State { + bool _isPressed = false; + + String get _displayText { + if (widget.startDate != null && widget.endDate != null) { + return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}'; + } else if (widget.startDate != null) { + return _formatDate(widget.startDate!); + } + return widget.placeholder; + } + + bool get _hasValue { + return widget.startDate != null || widget.endDate != null; + } + + 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}'; + } + + Future _showDateRangePicker() async { + if (!widget.enabled) return; + + final result = await DateRangePickerBottomSheet.show( + context: context, + title: widget.label ?? 'Pilih Rentang Tanggal', + initialStartDate: widget.startDate, + initialEndDate: widget.endDate, + minDate: widget.minDate, + maxDate: widget.maxDate, + primaryColor: widget.primaryColor, + onChanged: widget.onChanged, + ); + + if (result != null && widget.onChanged != null) { + if (result.value is PickerDateRange) { + final PickerDateRange range = result.value; + widget.onChanged!(range.startDate, range.endDate); + } + } + } + + @override + Widget build(BuildContext context) { + final hasError = widget.errorText != null && widget.errorText!.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Label + if (widget.label != null) ...[ + Text( + widget.label!, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: hasError ? AppColor.error : AppColor.textSecondary, + ), + ), + const SizedBox(height: 8), + ], + + // Input Field + GestureDetector( + onTap: _showDateRangePicker, + onTapDown: widget.enabled + ? (_) => setState(() => _isPressed = true) + : null, + onTapUp: widget.enabled + ? (_) => setState(() => _isPressed = false) + : null, + onTapCancel: widget.enabled + ? () => setState(() => _isPressed = false) + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + height: widget.height, + padding: + widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + decoration: + widget.decoration ?? + BoxDecoration( + color: widget.enabled + ? (_isPressed ? AppColor.backgroundLight : AppColor.white) + : AppColor.background, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: hasError + ? AppColor.error + : (_isPressed ? widget.primaryColor : AppColor.border), + width: _isPressed ? 2 : 1, + ), + boxShadow: _isPressed && widget.enabled + ? [ + BoxShadow( + color: widget.primaryColor.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Row( + children: [ + // Date Text + Expanded( + child: Text( + _displayText, + style: + widget.textStyle ?? + TextStyle( + fontSize: 15, + fontWeight: _hasValue + ? FontWeight.w500 + : FontWeight.w400, + color: widget.enabled + ? (_hasValue + ? AppColor.textPrimary + : AppColor.textSecondary) + : AppColor.textLight, + ), + ), + ), + + // Calendar Icon + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: widget.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.calendar_today_rounded, + size: 20, + color: widget.enabled + ? widget.primaryColor + : AppColor.textLight, + ), + ), + ], + ), + ), + ), + + // Error Text + if (hasError) ...[ + const SizedBox(height: 6), + Text( + widget.errorText!, + style: TextStyle( + fontSize: 12, + color: AppColor.error, + fontWeight: FontWeight.w400, + ), + ), + ], + ], + ); + } +} + +// Variasi dengan style yang berbeda +class DateRangePickerFieldOutlined extends StatefulWidget { + final String? label; + final String placeholder; + final DateTime? startDate; + final DateTime? endDate; + final DateTime? minDate; + final DateTime? maxDate; + final Function(DateTime? startDate, DateTime? endDate)? onChanged; + final Color primaryColor; + final bool enabled; + final String? errorText; + + const DateRangePickerFieldOutlined({ + Key? key, + this.label, + this.placeholder = 'Pilih rentang tanggal', + this.startDate, + this.endDate, + this.minDate, + this.maxDate, + this.onChanged, + this.primaryColor = AppColor.primary, + this.enabled = true, + this.errorText, + }) : super(key: key); + + @override + State createState() => + _DateRangePickerFieldOutlinedState(); +} + +class _DateRangePickerFieldOutlinedState + extends State { + bool _isFocused = false; + + String get _displayText { + if (widget.startDate != null && widget.endDate != null) { + return '${_formatDate(widget.startDate!)} - ${_formatDate(widget.endDate!)}'; + } else if (widget.startDate != null) { + return _formatDate(widget.startDate!); + } + return widget.placeholder; + } + + bool get _hasValue { + return widget.startDate != null || widget.endDate != null; + } + + 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}'; + } + + Future _showDateRangePicker() async { + if (!widget.enabled) return; + + setState(() => _isFocused = true); + + final result = await DateRangePickerBottomSheet.show( + context: context, + title: widget.label ?? 'Pilih Rentang Tanggal', + initialStartDate: widget.startDate, + initialEndDate: widget.endDate, + minDate: widget.minDate, + maxDate: widget.maxDate, + primaryColor: widget.primaryColor, + onChanged: widget.onChanged, + ); + + setState(() => _isFocused = false); + + if (result != null && widget.onChanged != null) { + if (result.value is PickerDateRange) { + final PickerDateRange range = result.value; + widget.onChanged!(range.startDate, range.endDate); + } + } + } + + @override + Widget build(BuildContext context) { + final hasError = widget.errorText != null && widget.errorText!.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: _showDateRangePicker, + child: Container( + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: hasError + ? AppColor.error + : (_isFocused || _hasValue + ? widget.primaryColor + : AppColor.border), + width: _isFocused ? 2 : 1, + ), + ), + child: Row( + children: [ + const SizedBox(width: 16), + + // Date Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.label != null && (_isFocused || _hasValue)) + Text( + widget.label!, + style: TextStyle( + fontSize: 12, + color: hasError + ? AppColor.error + : widget.primaryColor, + fontWeight: FontWeight.w500, + ), + ), + Text( + _hasValue + ? _displayText + : (widget.label ?? widget.placeholder), + style: TextStyle( + fontSize: _hasValue ? 16 : 16, + fontWeight: FontWeight.w400, + color: widget.enabled + ? (_hasValue + ? AppColor.textPrimary + : AppColor.textSecondary) + : AppColor.textLight, + ), + ), + ], + ), + ), + + // Calendar Icon + Padding( + padding: const EdgeInsets.only(right: 16), + child: Icon( + Icons.calendar_today_rounded, + size: 24, + color: widget.enabled + ? (_isFocused + ? widget.primaryColor + : AppColor.textSecondary) + : AppColor.textLight, + ), + ), + ], + ), + ), + ), + + // Error Text + if (hasError) ...[ + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + widget.errorText!, + style: TextStyle( + fontSize: 12, + color: AppColor.error, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ], + ); + } +} + +// Usage Example Widget +class DateRangePickerExample extends StatefulWidget { + @override + _DateRangePickerExampleState createState() => _DateRangePickerExampleState(); +} + +class _DateRangePickerExampleState extends State { + DateTime? _startDate; + DateTime? _endDate; + DateTime? _startDate2; + DateTime? _endDate2; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Date Range Picker Example'), + backgroundColor: AppColor.primary, + foregroundColor: AppColor.white, + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Default Style', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 16), + + DateRangePickerField( + label: 'Periode Laporan', + placeholder: 'Pilih tanggal mulai - selesai', + startDate: _startDate, + endDate: _endDate, + primaryColor: AppColor.primary, + onChanged: (start, end) { + setState(() { + _startDate = start; + _endDate = end; + }); + }, + ), + + const SizedBox(height: 32), + + Text( + 'Outlined Style', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 16), + + DateRangePickerFieldOutlined( + label: 'Rentang Waktu', + placeholder: 'Pilih rentang tanggal', + startDate: _startDate2, + endDate: _endDate2, + primaryColor: AppColor.secondary, + onChanged: (start, end) { + setState(() { + _startDate2 = start; + _endDate2 = end; + }); + }, + ), + + const SizedBox(height: 24), + + // Display selected dates + if (_startDate != null || + _endDate != null || + _startDate2 != null || + _endDate2 != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColor.background, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selected Dates:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + if (_startDate != null) + Text( + 'Default: ${_startDate!} - ${_endDate ?? 'Not selected'}', + ), + if (_startDate2 != null) + Text( + 'Outlined: ${_startDate2!} - ${_endDate2 ?? 'Not selected'}', + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/sales/sales_page.dart b/lib/presentation/pages/sales/sales_page.dart index 2b70f1f..70a0a64 100644 --- a/lib/presentation/pages/sales/sales_page.dart +++ b/lib/presentation/pages/sales/sales_page.dart @@ -10,6 +10,7 @@ import '../../../common/theme/theme.dart'; import '../../../domain/analytic/analytic.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; +import '../../components/field/date_range_picker_field.dart'; import '../../components/spacer/spacer.dart'; import 'widgets/summary_card.dart'; @@ -79,143 +80,125 @@ class _SalesPageState extends State with TickerProviderStateMixin { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, - body: BlocBuilder( - builder: (context, state) { - return CustomScrollView( - slivers: [ - // App Bar - SliverAppBar( - expandedHeight: 120, - floating: false, - pinned: true, - backgroundColor: AppColor.primary, - flexibleSpace: CustomAppBar(title: 'Penjualan'), - ), - - // Date Range Header - SliverToBoxAdapter( - child: SlideTransition( - position: slideAnimation, - child: FadeTransition( - opacity: fadeAnimation, - child: state.isFetching - ? _buildDateRangeShimmer() - : _buildDateRangeHeader(), - ), + body: BlocListener( + listenWhen: (previous, current) => + previous.dateFrom != current.dateFrom && + previous.dateTo != current.dateTo, + listener: (context, state) { + context.read().add(SalesLoaderEvent.fectched()); + }, + child: BlocBuilder( + builder: (context, state) { + return CustomScrollView( + slivers: [ + // App Bar + SliverAppBar( + expandedHeight: 120, + floating: false, + pinned: true, + backgroundColor: AppColor.primary, + flexibleSpace: CustomAppBar(title: 'Penjualan'), ), - ), - // Summary Cards - SliverToBoxAdapter( - child: SlideTransition( - position: slideAnimation, - child: FadeTransition( - opacity: fadeAnimation, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Summary', - style: AppStyle.xxl.copyWith( - fontWeight: FontWeight.bold, - color: AppColor.textPrimary, - ), - ), - const SpaceHeight(16), - state.isFetching - ? _buildSummaryShimmer() - : _buildSummaryCards(state), - ], - ), - ), - ), - ), - ), - - // Net Sales Card - SliverToBoxAdapter( - child: SlideTransition( - position: slideAnimation, - child: FadeTransition( - opacity: fadeAnimation, - child: state.isFetching - ? _buildNetSalesShimmer() - : _buildNetSalesCard(state), - ), - ), - ), - - // Daily Sales Section Header - SliverToBoxAdapter( - child: SlideTransition( - position: slideAnimation, - child: FadeTransition( - opacity: fadeAnimation, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Text( - 'Daily Breakdown', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColor.textPrimary, + // Date Range Header + SliverToBoxAdapter( + child: SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: fadeAnimation, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: DateRangePickerField( + maxDate: DateTime.now(), + startDate: state.dateFrom, + endDate: state.dateTo, + onChanged: (startDate, endDate) { + context.read().add( + SalesLoaderEvent.rangeDateChanged( + startDate!, + endDate!, + ), + ); + }, ), ), ), ), ), - ), - // Daily Sales List - state.isFetching - ? _buildDailySalesShimmer() - : _buildDailySalesList(state), - - // Bottom Padding - const SliverToBoxAdapter(child: SpaceHeight(32)), - ], - ); - }, - ), - ); - } - - // Shimmer Components - Widget _buildDateRangeShimmer() { - return Container( - margin: const EdgeInsets.all(16), - child: Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), + // Summary Cards + SliverToBoxAdapter( + child: SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: fadeAnimation, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Summary', + style: AppStyle.xxl.copyWith( + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + ), + const SpaceHeight(16), + state.isFetching + ? _buildSummaryShimmer() + : _buildSummaryCards(state), + ], + ), + ), + ), + ), ), - ), - SpaceWidth(8), - Container( - width: 150, - height: 16, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), + + // Net Sales Card + SliverToBoxAdapter( + child: SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: fadeAnimation, + child: state.isFetching + ? _buildNetSalesShimmer() + : _buildNetSalesCard(state), + ), + ), ), - ), - ], - ), + + // Daily Sales Section Header + SliverToBoxAdapter( + child: SlideTransition( + position: slideAnimation, + child: FadeTransition( + opacity: fadeAnimation, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Text( + 'Daily Breakdown', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColor.textPrimary, + ), + ), + ), + ), + ), + ), + + // Daily Sales List + state.isFetching + ? _buildDailySalesShimmer() + : _buildDailySalesList(state), + + // Bottom Padding + const SliverToBoxAdapter(child: SpaceHeight(32)), + ], + ); + }, ), ), ); @@ -415,38 +398,6 @@ class _SalesPageState extends State with TickerProviderStateMixin { ); } - // Original Components (preserved) - Widget _buildDateRangeHeader() { - return Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: BoxDecoration( - color: AppColor.surface, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - Icon(Icons.date_range, color: AppColor.primary, size: 20), - SpaceWidth(8), - Text( - 'Aug 1 - Aug 15, 2025', - style: AppStyle.md.copyWith( - color: AppColor.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } - Widget _buildSummaryCards(SalesLoaderState state) { return Column( children: [ diff --git a/pubspec.lock b/pubspec.lock index 40534f0..48ae405 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1218,6 +1218,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: @@ -1388,4 +1404,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.27.4" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 33a7a21..95f72d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: loader_overlay: ^5.0.0 shimmer: ^3.0.0 cached_network_image: ^3.4.1 + syncfusion_flutter_datepicker: ^30.2.5 dev_dependencies: flutter_test: