feat: sales range date

This commit is contained in:
efrilm 2025-08-18 16:43:07 +07:00
parent 2d25e15380
commit 1f6e5e9a2b
10 changed files with 1286 additions and 190 deletions

View File

@ -21,19 +21,26 @@ class SalesLoaderBloc extends Bloc<SalesLoaderEvent, SalesLoaderState> {
Future<void> _onSalesLoaderEvent(
SalesLoaderEvent event,
Emitter<SalesLoaderState> 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));
}
}

View File

@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$SalesLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fectched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fectched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fectched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fectched value) fectched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fectched value)? fectched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fectched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fectched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fectched value) fectched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fectched value)? fectched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fectched value)? fectched,
required TResult orElse(),
}) {
@ -174,6 +346,8 @@ mixin _$SalesLoaderState {
Option<AnalyticFailure> 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<AnalyticFailure> 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<AnalyticFailure> 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<AnalyticFailure> failureOptionSales,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$SalesLoaderStateImpl;
@override
@ -366,6 +581,10 @@ abstract class _SalesLoaderState implements SalesLoaderState {
Option<AnalyticFailure> 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.

View File

@ -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;
}

View File

@ -6,10 +6,14 @@ class SalesLoaderState with _$SalesLoaderState {
required SalesAnalytic sales,
required Option<AnalyticFailure> 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(),
);
}

View File

@ -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>()),
);

View File

@ -0,0 +1,363 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
class DateRangePickerBottomSheet {
static Future<DateRangePickerSelectionChangedArgs?> 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<DateRangePickerSelectionChangedArgs?>(
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<double> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_slideAnimation = Tween<double>(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,
),
),
),
),
],
),
),
),
],
),
),
);
},
);
}
}

View File

@ -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<DateRangePickerField> createState() => _DateRangePickerFieldState();
}
class _DateRangePickerFieldState extends State<DateRangePickerField> {
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<void> _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<DateRangePickerFieldOutlined> createState() =>
_DateRangePickerFieldOutlinedState();
}
class _DateRangePickerFieldOutlinedState
extends State<DateRangePickerFieldOutlined> {
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<void> _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<DateRangePickerExample> {
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'}',
),
],
),
),
],
),
),
);
}
}

View File

@ -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<SalesPage> with TickerProviderStateMixin {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: BlocBuilder<SalesLoaderBloc, SalesLoaderState>(
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<SalesLoaderBloc, SalesLoaderState>(
listenWhen: (previous, current) =>
previous.dateFrom != current.dateFrom &&
previous.dateTo != current.dateTo,
listener: (context, state) {
context.read<SalesLoaderBloc>().add(SalesLoaderEvent.fectched());
},
child: BlocBuilder<SalesLoaderBloc, SalesLoaderState>(
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<SalesLoaderBloc>().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<SalesPage> 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: [

View File

@ -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"

View File

@ -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: