feat: update sales ui
Some checks are pending
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run

This commit is contained in:
Efril 2026-06-23 23:18:22 +07:00
parent 7137cd2335
commit 8d801e52d9
37 changed files with 3234 additions and 833 deletions

View File

@ -25,6 +25,9 @@ class CategoryAnalyticLoaderBloc
Emitter<CategoryAnalyticLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
fetched: (e) async {
emit(
state.copyWith(
@ -34,8 +37,8 @@ class CategoryAnalyticLoaderBloc
);
final result = await _repository.getCategory(
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
var data = result.fold(

View File

@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$CategoryAnalyticLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@ -74,6 +81,165 @@ class _$CategoryAnalyticLoaderEventCopyWithImpl<
/// 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
_$CategoryAnalyticLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of CategoryAnalyticLoaderEvent
/// 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 'CategoryAnalyticLoaderEvent.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 CategoryAnalyticLoaderEvent
/// 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() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
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(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements CategoryAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of CategoryAnalyticLoaderEvent
/// 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 _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
@ -116,19 +282,27 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
@ -141,6 +315,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
@ -149,6 +324,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
@ -157,6 +333,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
@ -177,6 +354,8 @@ mixin _$CategoryAnalyticLoaderState {
Option<AnalyticFailure> get failureOptionCategoryAnalytic =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of CategoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@ -200,6 +379,8 @@ abstract class $CategoryAnalyticLoaderStateCopyWith<$Res> {
CategoryAnalytic categoryAnalytic,
Option<AnalyticFailure> failureOptionCategoryAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
$CategoryAnalyticCopyWith<$Res> get categoryAnalytic;
@ -226,6 +407,8 @@ class _$CategoryAnalyticLoaderStateCopyWithImpl<
Object? categoryAnalytic = null,
Object? failureOptionCategoryAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
@ -241,6 +424,14 @@ class _$CategoryAnalyticLoaderStateCopyWithImpl<
? _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,
);
@ -270,6 +461,8 @@ abstract class _$$CategoryAnalyticLoaderStateImplCopyWith<$Res>
CategoryAnalytic categoryAnalytic,
Option<AnalyticFailure> failureOptionCategoryAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
@override
@ -297,6 +490,8 @@ class __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$Res>
Object? categoryAnalytic = null,
Object? failureOptionCategoryAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$CategoryAnalyticLoaderStateImpl(
@ -312,6 +507,14 @@ class __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$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,
),
);
}
@ -325,6 +528,8 @@ class _$CategoryAnalyticLoaderStateImpl
required this.categoryAnalytic,
required this.failureOptionCategoryAnalytic,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
});
@override
@ -334,10 +539,14 @@ class _$CategoryAnalyticLoaderStateImpl
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'CategoryAnalyticLoaderState(categoryAnalytic: $categoryAnalytic, failureOptionCategoryAnalytic: $failureOptionCategoryAnalytic, isFetching: $isFetching)';
return 'CategoryAnalyticLoaderState(categoryAnalytic: $categoryAnalytic, failureOptionCategoryAnalytic: $failureOptionCategoryAnalytic, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
@ -354,7 +563,10 @@ class _$CategoryAnalyticLoaderStateImpl
other.failureOptionCategoryAnalytic ==
failureOptionCategoryAnalytic) &&
(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
@ -363,6 +575,8 @@ class _$CategoryAnalyticLoaderStateImpl
categoryAnalytic,
failureOptionCategoryAnalytic,
isFetching,
dateFrom,
dateTo,
);
/// Create a copy of CategoryAnalyticLoaderState
@ -383,6 +597,8 @@ abstract class _CategoryAnalyticLoaderState
required final CategoryAnalytic categoryAnalytic,
required final Option<AnalyticFailure> failureOptionCategoryAnalytic,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$CategoryAnalyticLoaderStateImpl;
@override
@ -391,6 +607,10 @@ abstract class _CategoryAnalyticLoaderState
Option<AnalyticFailure> get failureOptionCategoryAnalytic;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of CategoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.

View File

@ -2,5 +2,9 @@ part of 'category_analytic_loader_bloc.dart';
@freezed
class CategoryAnalyticLoaderEvent with _$CategoryAnalyticLoaderEvent {
const factory CategoryAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory CategoryAnalyticLoaderEvent.fetched() = _Fetched;
}

View File

@ -6,10 +6,14 @@ class CategoryAnalyticLoaderState with _$CategoryAnalyticLoaderState {
required CategoryAnalytic categoryAnalytic,
required Option<AnalyticFailure> failureOptionCategoryAnalytic,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
}) = _CategoryAnalyticLoaderState;
factory CategoryAnalyticLoaderState.initial() => CategoryAnalyticLoaderState(
categoryAnalytic: CategoryAnalytic.empty(),
failureOptionCategoryAnalytic: none(),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}

View File

@ -28,6 +28,9 @@ class PaymentMethodAnalyticLoaderBloc
Emitter<PaymentMethodAnalyticLoaderState> emit,
) {
return event.map(
rangeDateChanged: (e) async {
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
},
fetched: (e) async {
emit(
state.copyWith(
@ -37,8 +40,8 @@ class PaymentMethodAnalyticLoaderBloc
);
final result = await _repository.getPaymentMethod(
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
dateFrom: state.dateFrom,
dateTo: state.dateTo,
);
var data = result.fold(

View File

@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$PaymentMethodAnalyticLoaderEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@ -74,6 +81,168 @@ class _$PaymentMethodAnalyticLoaderEventCopyWithImpl<
/// 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
_$PaymentMethodAnalyticLoaderEventCopyWithImpl<
$Res,
_$RangeDateChangedImpl
>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of PaymentMethodAnalyticLoaderEvent
/// 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 'PaymentMethodAnalyticLoaderEvent.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 PaymentMethodAnalyticLoaderEvent
/// 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() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return rangeDateChanged?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
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(_Fetched value) fetched,
}) {
return rangeDateChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return rangeDateChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements PaymentMethodAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of PaymentMethodAnalyticLoaderEvent
/// 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 _$$FetchedImplCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
@ -116,19 +285,27 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({required TResult Function() fetched}) {
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function() fetched,
}) {
return fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({TResult? Function()? fetched}) {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult Function()? fetched,
required TResult orElse(),
}) {
@ -141,6 +318,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_RangeDateChanged value) rangeDateChanged,
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
@ -149,6 +327,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
@ -157,6 +336,7 @@ class _$FetchedImpl implements _Fetched {
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_RangeDateChanged value)? rangeDateChanged,
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
@ -178,6 +358,8 @@ mixin _$PaymentMethodAnalyticLoaderState {
Option<AnalyticFailure> get failureOptionPaymentMethodAnalytic =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
DateTime get dateFrom => throw _privateConstructorUsedError;
DateTime get dateTo => throw _privateConstructorUsedError;
/// Create a copy of PaymentMethodAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@ -201,6 +383,8 @@ abstract class $PaymentMethodAnalyticLoaderStateCopyWith<$Res> {
PaymentMethodAnalytic paymentMethodAnalytic,
Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
$PaymentMethodAnalyticCopyWith<$Res> get paymentMethodAnalytic;
@ -227,6 +411,8 @@ class _$PaymentMethodAnalyticLoaderStateCopyWithImpl<
Object? paymentMethodAnalytic = null,
Object? failureOptionPaymentMethodAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_value.copyWith(
@ -243,6 +429,14 @@ class _$PaymentMethodAnalyticLoaderStateCopyWithImpl<
? _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,
);
@ -274,6 +468,8 @@ abstract class _$$PaymentMethodAnalyticLoaderStateImplCopyWith<$Res>
PaymentMethodAnalytic paymentMethodAnalytic,
Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
});
@override
@ -301,6 +497,8 @@ class __$$PaymentMethodAnalyticLoaderStateImplCopyWithImpl<$Res>
Object? paymentMethodAnalytic = null,
Object? failureOptionPaymentMethodAnalytic = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
}) {
return _then(
_$PaymentMethodAnalyticLoaderStateImpl(
@ -317,6 +515,14 @@ class __$$PaymentMethodAnalyticLoaderStateImplCopyWithImpl<$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,
),
);
}
@ -330,6 +536,8 @@ class _$PaymentMethodAnalyticLoaderStateImpl
required this.paymentMethodAnalytic,
required this.failureOptionPaymentMethodAnalytic,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
});
@override
@ -339,10 +547,14 @@ class _$PaymentMethodAnalyticLoaderStateImpl
@override
@JsonKey()
final bool isFetching;
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'PaymentMethodAnalyticLoaderState(paymentMethodAnalytic: $paymentMethodAnalytic, failureOptionPaymentMethodAnalytic: $failureOptionPaymentMethodAnalytic, isFetching: $isFetching)';
return 'PaymentMethodAnalyticLoaderState(paymentMethodAnalytic: $paymentMethodAnalytic, failureOptionPaymentMethodAnalytic: $failureOptionPaymentMethodAnalytic, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
@ -359,7 +571,10 @@ class _$PaymentMethodAnalyticLoaderStateImpl
other.failureOptionPaymentMethodAnalytic ==
failureOptionPaymentMethodAnalytic) &&
(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
@ -368,6 +583,8 @@ class _$PaymentMethodAnalyticLoaderStateImpl
paymentMethodAnalytic,
failureOptionPaymentMethodAnalytic,
isFetching,
dateFrom,
dateTo,
);
/// Create a copy of PaymentMethodAnalyticLoaderState
@ -390,6 +607,8 @@ abstract class _PaymentMethodAnalyticLoaderState
required final PaymentMethodAnalytic paymentMethodAnalytic,
required final Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
}) = _$PaymentMethodAnalyticLoaderStateImpl;
@override
@ -398,6 +617,10 @@ abstract class _PaymentMethodAnalyticLoaderState
Option<AnalyticFailure> get failureOptionPaymentMethodAnalytic;
@override
bool get isFetching;
@override
DateTime get dateFrom;
@override
DateTime get dateTo;
/// Create a copy of PaymentMethodAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.

View File

@ -2,5 +2,9 @@ part of 'payment_method_analytic_loader_bloc.dart';
@freezed
class PaymentMethodAnalyticLoaderEvent with _$PaymentMethodAnalyticLoaderEvent {
const factory PaymentMethodAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory PaymentMethodAnalyticLoaderEvent.fetched() = _Fetched;
}

View File

@ -6,11 +6,15 @@ class PaymentMethodAnalyticLoaderState with _$PaymentMethodAnalyticLoaderState {
required PaymentMethodAnalytic paymentMethodAnalytic,
required Option<AnalyticFailure> failureOptionPaymentMethodAnalytic,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
}) = _PaymentMethodAnalyticLoaderState;
factory PaymentMethodAnalyticLoaderState.initial() =>
PaymentMethodAnalyticLoaderState(
paymentMethodAnalytic: PaymentMethodAnalytic.empty(),
failureOptionPaymentMethodAnalytic: none(),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}

View File

@ -13,7 +13,7 @@ class ProductAnalyticLoaderState with _$ProductAnalyticLoaderState {
factory ProductAnalyticLoaderState.initial() => ProductAnalyticLoaderState(
productAnalytic: ProductAnalytic.empty(),
failureOptionProductAnalytic: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}

View File

@ -13,7 +13,7 @@ class SalesLoaderState with _$SalesLoaderState {
factory SalesLoaderState.initial() => SalesLoaderState(
sales: SalesAnalytic.empty(),
failureOptionSales: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateFrom: DateTime.now(),
dateTo: DateTime.now(),
);
}

View File

@ -9,6 +9,11 @@ part 'app_value.dart';
class ThemeApp {
static ThemeData get theme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColor.primary,
primary: AppColor.primary,
brightness: Brightness.light,
),
scaffoldBackgroundColor: AppColor.background,
fontFamily: FontFamily.quicksand,
inputDecorationTheme: InputDecorationTheme(
@ -67,6 +72,7 @@ class ThemeApp {
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColor.white,
)
surfaceTintColor: Colors.transparent,
),
);
}

View File

@ -10,7 +10,7 @@ class ApiPath {
static const String dashboardAnalytic = '/api/v1/analytics/dashboard';
static const String productAnalytic = '/api/v1/analytics/products';
static const String paymentMethodAnalytic =
'/api/v1/analytics/paymentMethods';
'/api/v1/analytics/payment-methods';
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
static const String exclusiveSummaryAnalytic =
'/api/v1/analytics/exclusive-summary/period';

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ class CategoryAnalytic with _$CategoryAnalytic {
const factory CategoryAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required List<CategoryAnalyticItem> data,
@ -13,6 +14,7 @@ class CategoryAnalytic with _$CategoryAnalytic {
factory CategoryAnalytic.empty() => const CategoryAnalytic(
organizationId: "",
outletId: "",
outletName: "",
dateFrom: "",
dateTo: "",
data: [],

View File

@ -5,6 +5,7 @@ class DashboardAnalytic with _$DashboardAnalytic {
const factory DashboardAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required DashboardOverview overview,
@ -16,6 +17,7 @@ class DashboardAnalytic with _$DashboardAnalytic {
factory DashboardAnalytic.empty() => DashboardAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
overview: DashboardOverview.empty(),
@ -34,6 +36,9 @@ class DashboardOverview with _$DashboardOverview {
required int totalCustomers,
required int voidedOrders,
required int refundedOrders,
required int totalItemSold,
required int totalLowStock,
required int totalProductActive,
}) = _DashboardOverview;
factory DashboardOverview.empty() => const DashboardOverview(
@ -43,6 +48,9 @@ class DashboardOverview with _$DashboardOverview {
totalCustomers: 0,
voidedOrders: 0,
refundedOrders: 0,
totalItemSold: 0,
totalLowStock: 0,
totalProductActive: 0,
);
}

View File

@ -5,6 +5,7 @@ class ExclusiveSummary with _$ExclusiveSummary {
const factory ExclusiveSummary({
required String organizationId,
required String outletId,
required String outletName,
required ExclusiveSummaryPeriod period,
required ExclusiveSummarySummary summary,
required ExclusiveSummaryReimburse reimburse,
@ -17,6 +18,7 @@ class ExclusiveSummary with _$ExclusiveSummary {
factory ExclusiveSummary.empty() => ExclusiveSummary(
organizationId: '',
outletId: '',
outletName: '',
period: ExclusiveSummaryPeriod.empty(),
summary: ExclusiveSummarySummary.empty(),
reimburse: ExclusiveSummaryReimburse.empty(),
@ -79,8 +81,7 @@ class ExclusiveSummaryReimburse with _$ExclusiveSummaryReimburse {
required int totalReimburse,
}) = _ExclusiveSummaryReimburse;
factory ExclusiveSummaryReimburse.empty() =>
const ExclusiveSummaryReimburse(
factory ExclusiveSummaryReimburse.empty() => const ExclusiveSummaryReimburse(
totalCost: 0,
excludedSalaryStaff: 0,
totalReimburse: 0,

View File

@ -5,6 +5,7 @@ class PaymentMethodAnalytic with _$PaymentMethodAnalytic {
const factory PaymentMethodAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required String groupBy,
@ -15,6 +16,7 @@ class PaymentMethodAnalytic with _$PaymentMethodAnalytic {
factory PaymentMethodAnalytic.empty() => PaymentMethodAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
groupBy: '',

View File

@ -5,6 +5,7 @@ class ProductAnalytic with _$ProductAnalytic {
const factory ProductAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required String dateFrom,
required String dateTo,
required List<ProductAnalyticData> data,
@ -13,6 +14,7 @@ class ProductAnalytic with _$ProductAnalytic {
factory ProductAnalytic.empty() => const ProductAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: '',
dateTo: '',
data: [],
@ -24,22 +26,40 @@ class ProductAnalyticData with _$ProductAnalyticData {
const factory ProductAnalyticData({
required String productId,
required String productName,
required String productSku,
required int productPrice,
required String categoryId,
required String categoryName,
required int categoryOrder,
required int quantitySold,
required int revenue,
required double averagePrice,
required int orderCount,
required int standardHppPerUnit,
required int standardHppTotal,
required int fifoHppPerUnit,
required int fifoHppTotal,
required int movingAverageHppPerUnit,
required int movingAverageHppTotal,
}) = _ProductAnalyticData;
factory ProductAnalyticData.empty() => const ProductAnalyticData(
productId: '',
productName: '',
productSku: '',
productPrice: 0,
categoryId: '',
categoryName: '',
categoryOrder: 0,
quantitySold: 0,
revenue: 0,
averagePrice: 0.0,
orderCount: 0,
standardHppPerUnit: 0,
standardHppTotal: 0,
fifoHppPerUnit: 0,
fifoHppTotal: 0,
movingAverageHppPerUnit: 0,
movingAverageHppTotal: 0,
);
}

View File

@ -5,6 +5,7 @@ class SalesAnalytic with _$SalesAnalytic {
const factory SalesAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required String groupBy,
@ -15,6 +16,7 @@ class SalesAnalytic with _$SalesAnalytic {
factory SalesAnalytic.empty() => SalesAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
groupBy: '',

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ _$SalesAnalyticDtoImpl _$$SalesAnalyticDtoImplFromJson(
) => _$SalesAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] == null
? null
: DateTime.parse(json['date_from'] as String),
@ -33,6 +34,7 @@ Map<String, dynamic> _$$SalesAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
'group_by': instance.groupBy,
@ -219,6 +221,7 @@ _$CategoryAnalyticDtoImpl _$$CategoryAnalyticDtoImplFromJson(
) => _$CategoryAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
data: (json['data'] as List<dynamic>?)
@ -231,6 +234,7 @@ Map<String, dynamic> _$$CategoryAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'data': instance.data,
@ -391,6 +395,7 @@ _$DashboardAnalyticDtoImpl _$$DashboardAnalyticDtoImplFromJson(
) => _$DashboardAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
overview: json['overview'] == null
@ -414,6 +419,7 @@ Map<String, dynamic> _$$DashboardAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'overview': instance.overview,
@ -431,6 +437,9 @@ _$DashboardOverviewDtoImpl _$$DashboardOverviewDtoImplFromJson(
totalCustomers: (json['total_customers'] as num?)?.toInt(),
voidedOrders: (json['voided_orders'] as num?)?.toInt(),
refundedOrders: (json['refunded_orders'] as num?)?.toInt(),
totalItemSold: (json['total_item_sold'] as num?)?.toInt(),
totalLowStock: (json['total_low_stock'] as num?)?.toInt(),
totalProductActive: (json['total_product_active'] as num?)?.toInt(),
);
Map<String, dynamic> _$$DashboardOverviewDtoImplToJson(
@ -442,6 +451,9 @@ Map<String, dynamic> _$$DashboardOverviewDtoImplToJson(
'total_customers': instance.totalCustomers,
'voided_orders': instance.voidedOrders,
'refunded_orders': instance.refundedOrders,
'total_item_sold': instance.totalItemSold,
'total_low_stock': instance.totalLowStock,
'total_product_active': instance.totalProductActive,
};
_$DashboardTopProductDtoImpl _$$DashboardTopProductDtoImplFromJson(
@ -523,6 +535,7 @@ _$ProductAnalyticDtoImpl _$$ProductAnalyticDtoImplFromJson(
) => _$ProductAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
data: (json['data'] as List<dynamic>?)
@ -535,6 +548,7 @@ Map<String, dynamic> _$$ProductAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'data': instance.data,
@ -545,12 +559,22 @@ _$ProductAnalyticDataDtoImpl _$$ProductAnalyticDataDtoImplFromJson(
) => _$ProductAnalyticDataDtoImpl(
productId: json['product_id'] as String?,
productName: json['product_name'] as String?,
productSku: json['product_sku'] as String?,
productPrice: (json['product_price'] as num?)?.toInt(),
categoryId: json['category_id'] as String?,
categoryName: json['category_name'] as String?,
categoryOrder: (json['category_order'] as num?)?.toInt(),
quantitySold: (json['quantity_sold'] as num?)?.toInt(),
revenue: (json['revenue'] as num?)?.toInt(),
averagePrice: (json['average_price'] as num?)?.toDouble(),
orderCount: (json['order_count'] as num?)?.toInt(),
standardHppPerUnit: (json['standard_hpp_per_unit'] as num?)?.toInt(),
standardHppTotal: (json['standard_hpp_total'] as num?)?.toInt(),
fifoHppPerUnit: (json['fifo_hpp_per_unit'] as num?)?.toInt(),
fifoHppTotal: (json['fifo_hpp_total'] as num?)?.toInt(),
movingAverageHppPerUnit: (json['moving_average_hpp_per_unit'] as num?)
?.toInt(),
movingAverageHppTotal: (json['moving_average_hpp_total'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ProductAnalyticDataDtoImplToJson(
@ -558,12 +582,21 @@ Map<String, dynamic> _$$ProductAnalyticDataDtoImplToJson(
) => <String, dynamic>{
'product_id': instance.productId,
'product_name': instance.productName,
'product_sku': instance.productSku,
'product_price': instance.productPrice,
'category_id': instance.categoryId,
'category_name': instance.categoryName,
'category_order': instance.categoryOrder,
'quantity_sold': instance.quantitySold,
'revenue': instance.revenue,
'average_price': instance.averagePrice,
'order_count': instance.orderCount,
'standard_hpp_per_unit': instance.standardHppPerUnit,
'standard_hpp_total': instance.standardHppTotal,
'fifo_hpp_per_unit': instance.fifoHppPerUnit,
'fifo_hpp_total': instance.fifoHppTotal,
'moving_average_hpp_per_unit': instance.movingAverageHppPerUnit,
'moving_average_hpp_total': instance.movingAverageHppTotal,
};
_$PaymentMethodAnalyticDtoImpl _$$PaymentMethodAnalyticDtoImplFromJson(
@ -571,6 +604,7 @@ _$PaymentMethodAnalyticDtoImpl _$$PaymentMethodAnalyticDtoImplFromJson(
) => _$PaymentMethodAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] as String?,
dateTo: json['date_to'] as String?,
groupBy: json['group_by'] as String?,
@ -589,6 +623,7 @@ Map<String, dynamic> _$$PaymentMethodAnalyticDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom,
'date_to': instance.dateTo,
'group_by': instance.groupBy,
@ -795,6 +830,7 @@ _$ExclusiveSummaryDtoImpl _$$ExclusiveSummaryDtoImplFromJson(
) => _$ExclusiveSummaryDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
period: json['period'] == null
? null
: ExclusiveSummaryPeriodDto.fromJson(
@ -839,6 +875,7 @@ Map<String, dynamic> _$$ExclusiveSummaryDtoImplToJson(
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'period': instance.period,
'summary': instance.summary,
'reimburse': instance.reimburse,

View File

@ -7,6 +7,7 @@ class CategoryAnalyticDto with _$CategoryAnalyticDto {
const factory CategoryAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'data') List<CategoryAnalyticItemDto>? data,
@ -18,6 +19,7 @@ class CategoryAnalyticDto with _$CategoryAnalyticDto {
CategoryAnalytic toDomain() => CategoryAnalytic(
organizationId: organizationId ?? "",
outletId: outletId ?? "",
outletName: outletName ?? "",
dateFrom: dateFrom ?? "",
dateTo: dateTo ?? "",
data: data?.map((e) => e.toDomain()).toList() ?? [],

View File

@ -7,6 +7,7 @@ class DashboardAnalyticDto with _$DashboardAnalyticDto {
const factory DashboardAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'overview') DashboardOverviewDto? overview,
@ -22,6 +23,7 @@ class DashboardAnalyticDto with _$DashboardAnalyticDto {
DashboardAnalytic toDomain() => DashboardAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? '',
overview: overview?.toDomain() ?? DashboardOverview.empty(),
@ -42,6 +44,9 @@ class DashboardOverviewDto with _$DashboardOverviewDto {
@JsonKey(name: 'total_customers') int? totalCustomers,
@JsonKey(name: 'voided_orders') int? voidedOrders,
@JsonKey(name: 'refunded_orders') int? refundedOrders,
@JsonKey(name: 'total_item_sold') int? totalItemSold,
@JsonKey(name: 'total_low_stock') int? totalLowStock,
@JsonKey(name: 'total_product_active') int? totalProductActive,
}) = _DashboardOverviewDto;
factory DashboardOverviewDto.fromJson(Map<String, dynamic> json) =>
@ -54,6 +59,9 @@ class DashboardOverviewDto with _$DashboardOverviewDto {
totalCustomers: totalCustomers ?? 0,
voidedOrders: voidedOrders ?? 0,
refundedOrders: refundedOrders ?? 0,
totalItemSold: totalItemSold ?? 0,
totalLowStock: totalLowStock ?? 0,
totalProductActive: totalProductActive ?? 0,
);
}

View File

@ -7,6 +7,7 @@ class ExclusiveSummaryDto with _$ExclusiveSummaryDto {
const factory ExclusiveSummaryDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'period') ExclusiveSummaryPeriodDto? period,
@JsonKey(name: 'summary') ExclusiveSummarySummaryDto? summary,
@JsonKey(name: 'reimburse') ExclusiveSummaryReimburseDto? reimburse,
@ -26,6 +27,7 @@ class ExclusiveSummaryDto with _$ExclusiveSummaryDto {
ExclusiveSummary toDomain() => ExclusiveSummary(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
period: period?.toDomain() ?? ExclusiveSummaryPeriod.empty(),
summary: summary?.toDomain() ?? ExclusiveSummarySummary.empty(),
reimburse: reimburse?.toDomain() ?? ExclusiveSummaryReimburse.empty(),

View File

@ -7,6 +7,7 @@ class PaymentMethodAnalyticDto with _$PaymentMethodAnalyticDto {
const factory PaymentMethodAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@ -21,6 +22,7 @@ class PaymentMethodAnalyticDto with _$PaymentMethodAnalyticDto {
return PaymentMethodAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? '',
dateTo: dateTo ?? '',
groupBy: groupBy ?? '',

View File

@ -7,6 +7,7 @@ class ProductAnalyticDto with _$ProductAnalyticDto {
const factory ProductAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') String? dateFrom,
@JsonKey(name: 'date_to') String? dateTo,
@JsonKey(name: 'data') List<ProductAnalyticDataDto>? data,
@ -18,6 +19,7 @@ class ProductAnalyticDto with _$ProductAnalyticDto {
ProductAnalytic toDomain() => ProductAnalytic(
organizationId: organizationId ?? "",
outletId: outletId ?? "",
outletName: outletName ?? "",
dateFrom: dateFrom ?? "",
dateTo: dateTo ?? "",
data: data?.map((e) => e.toDomain()).toList() ?? [],
@ -31,12 +33,21 @@ class ProductAnalyticDataDto with _$ProductAnalyticDataDto {
const factory ProductAnalyticDataDto({
@JsonKey(name: 'product_id') String? productId,
@JsonKey(name: 'product_name') String? productName,
@JsonKey(name: 'product_sku') String? productSku,
@JsonKey(name: 'product_price') int? productPrice,
@JsonKey(name: 'category_id') String? categoryId,
@JsonKey(name: 'category_name') String? categoryName,
@JsonKey(name: 'category_order') int? categoryOrder,
@JsonKey(name: 'quantity_sold') int? quantitySold,
@JsonKey(name: 'revenue') int? revenue,
@JsonKey(name: 'average_price') double? averagePrice,
@JsonKey(name: 'order_count') int? orderCount,
@JsonKey(name: 'standard_hpp_per_unit') int? standardHppPerUnit,
@JsonKey(name: 'standard_hpp_total') int? standardHppTotal,
@JsonKey(name: 'fifo_hpp_per_unit') int? fifoHppPerUnit,
@JsonKey(name: 'fifo_hpp_total') int? fifoHppTotal,
@JsonKey(name: 'moving_average_hpp_per_unit') int? movingAverageHppPerUnit,
@JsonKey(name: 'moving_average_hpp_total') int? movingAverageHppTotal,
}) = _ProductAnalyticDataDto;
factory ProductAnalyticDataDto.fromJson(Map<String, dynamic> json) =>
@ -45,11 +56,20 @@ class ProductAnalyticDataDto with _$ProductAnalyticDataDto {
ProductAnalyticData toDomain() => ProductAnalyticData(
productId: productId ?? "",
productName: productName ?? "",
productSku: productSku ?? "",
productPrice: productPrice ?? 0,
categoryId: categoryId ?? "",
categoryName: categoryName ?? "",
categoryOrder: categoryOrder ?? 0,
quantitySold: quantitySold ?? 0,
revenue: revenue ?? 0,
averagePrice: averagePrice ?? 0,
orderCount: orderCount ?? 0,
standardHppPerUnit: standardHppPerUnit ?? 0,
standardHppTotal: standardHppTotal ?? 0,
fifoHppPerUnit: fifoHppPerUnit ?? 0,
fifoHppTotal: fifoHppTotal ?? 0,
movingAverageHppPerUnit: movingAverageHppPerUnit ?? 0,
movingAverageHppTotal: movingAverageHppTotal ?? 0,
);
}

View File

@ -7,6 +7,7 @@ class SalesAnalyticDto with _$SalesAnalyticDto {
const factory SalesAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@ -20,6 +21,7 @@ class SalesAnalyticDto with _$SalesAnalyticDto {
SalesAnalytic toDomain() => SalesAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo ?? DateTime.fromMillisecondsSinceEpoch(0),
groupBy: groupBy ?? '',

View File

@ -52,6 +52,10 @@ class $AssetsIconsGen {
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/ic_launcher.png
AssetGenImage get icLauncher =>
const AssetGenImage('assets/images/ic_launcher.png');
/// File path: assets/images/ic_notification.png
AssetGenImage get icNotification =>
const AssetGenImage('assets/images/ic_notification.png');
@ -60,7 +64,7 @@ class $AssetsImagesGen {
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
/// List of all assets
List<AssetGenImage> get values => [icNotification, logo];
List<AssetGenImage> get values => [icLauncher, icNotification, logo];
}
class Assets {

View File

@ -13,13 +13,14 @@ class DateRangePickerBottomSheet {
DateTime? maxDate,
String? confirmText,
String? cancelText,
Color primaryColor = Colors.blue,
Color primaryColor = const Color(0xFFD90000),
Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async {
return await showModalBottomSheet<DateRangePickerSelectionChangedArgs?>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.transparent,
isDismissible: false,
enableDrag: false,
builder: (BuildContext context) => _DateRangePickerBottomSheet(
@ -71,6 +72,8 @@ class _DateRangePickerBottomSheetState
DateRangePickerSelectionChangedArgs? _selectionChangedArgs;
late AnimationController _animationController;
late Animation<double> _slideAnimation;
final DateRangePickerController _pickerController =
DateRangePickerController();
@override
void initState() {
@ -88,6 +91,7 @@ class _DateRangePickerBottomSheetState
@override
void dispose() {
_animationController.dispose();
_pickerController.dispose();
super.dispose();
}
@ -135,10 +139,75 @@ class _DateRangePickerBottomSheetState
return false;
}
void _applyQuickFilter(DateTime start, DateTime end) {
final range = PickerDateRange(start, end);
_pickerController.selectedRange = range;
setState(() {
_selectionChangedArgs = DateRangePickerSelectionChangedArgs(range);
});
}
Widget _buildQuickFilters() {
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
// This week (Monday to today)
final weekday = now.weekday; // 1=Mon, 7=Sun
final weekStart = todayStart.subtract(Duration(days: weekday - 1));
// This month (1st to today)
final monthStart = DateTime(now.year, now.month, 1);
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildFilterChip(
context.lang.today,
() => _applyQuickFilter(todayStart, todayStart),
),
_buildFilterChip(
'Minggu ini',
() => _applyQuickFilter(weekStart, todayStart),
),
_buildFilterChip(
'Bulan ini',
() => _applyQuickFilter(monthStart, todayStart),
),
_buildFilterChip(
'MTD',
() => _applyQuickFilter(monthStart, todayStart),
),
],
);
}
Widget _buildFilterChip(String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: widget.primaryColor.withOpacity(0.3)),
),
child: Text(
label,
style: TextStyle(
color: widget.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
);
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final bottomSheetHeight = screenHeight * 0.75;
final bottomSheetHeight = screenHeight * 0.85;
return AnimatedBuilder(
animation: _animationController,
@ -209,7 +278,12 @@ class _DateRangePickerBottomSheetState
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
// Quick filter chips
_buildQuickFilters(),
const SizedBox(height: 16),
// Date Picker
Container(
@ -221,8 +295,10 @@ class _DateRangePickerBottomSheetState
),
),
child: SfDateRangePicker(
controller: _pickerController,
onSelectionChanged: _onSelectionChanged,
selectionMode: DateRangePickerSelectionMode.range,
backgroundColor: Colors.white,
initialSelectedRange:
(widget.initialStartDate != null &&
widget.initialEndDate != null)
@ -233,6 +309,7 @@ class _DateRangePickerBottomSheetState
: null,
minDate: widget.minDate,
maxDate: widget.maxDate,
selectionColor: widget.primaryColor,
startRangeSelectionColor: widget.primaryColor,
endRangeSelectionColor: widget.primaryColor,
rangeSelectionColor: widget.primaryColor
@ -249,7 +326,7 @@ class _DateRangePickerBottomSheetState
),
monthViewSettings: DateRangePickerMonthViewSettings(
viewHeaderStyle: DateRangePickerViewHeaderStyle(
backgroundColor: Colors.grey.withOpacity(0.1),
backgroundColor: Colors.white,
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,

View File

@ -1,18 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shimmer/shimmer.dart';
import 'dart:math' as math;
import '../../../application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart';
import '../../../application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart';
import '../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart';
import '../../../application/analytic/sales_loader/sales_loader_bloc.dart';
import '../../../common/extension/extension.dart';
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';
import 'widgets/sales_category_card.dart';
import 'widgets/sales_header.dart';
import 'widgets/sales_payment_method_card.dart';
import 'widgets/sales_rincian_card.dart';
import 'widgets/sales_top_products_card.dart';
@RoutePage()
class SalesPage extends StatefulWidget implements AutoRouteWrapper {
@ -22,57 +23,58 @@ class SalesPage extends StatefulWidget implements AutoRouteWrapper {
State<SalesPage> createState() => _SalesPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<SalesLoaderBloc>()..add(SalesLoaderEvent.fectched()),
),
BlocProvider(
create: (context) =>
getIt<PaymentMethodAnalyticLoaderBloc>()
..add(const PaymentMethodAnalyticLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<CategoryAnalyticLoaderBloc>()
..add(const CategoryAnalyticLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<ProductAnalyticLoaderBloc>()
..add(const ProductAnalyticLoaderEvent.fetched()),
),
],
child: this,
);
}
class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
late AnimationController slideAnimationController;
late Animation<Offset> slideAnimation;
late AnimationController fadeAnimationController;
late Animation<double> fadeAnimation;
class _SalesPageState extends State<SalesPage>
with SingleTickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
// Slide Animation
slideAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(
parent: slideAnimationController,
curve: Curves.easeOutCubic,
),
);
// Fade Animation
fadeAnimationController = AnimationController(
_fadeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: fadeAnimationController, curve: Curves.easeOut),
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeOut,
);
// Start animations
Future.delayed(const Duration(milliseconds: 300), () {
slideAnimationController.forward();
fadeAnimationController.forward();
_fadeController.forward();
});
}
@override
void dispose() {
slideAnimationController.dispose();
fadeAnimationController.dispose();
_fadeController.dispose();
super.dispose();
}
@ -80,119 +82,97 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
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) {
body: BlocBuilder<SalesLoaderBloc, SalesLoaderState>(
builder: (context, salesState) {
return CustomScrollView(
slivers: [
// App Bar
SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
flexibleSpace: CustomAppBar(title: context.lang.sales),
// Header
SliverToBoxAdapter(
child: SalesHeader(
state: salesState,
onDateRangeChanged: (startDate, endDate) {
_onDateRangeChanged(context, startDate, endDate);
},
),
),
// Date Range Header
// Rincian Penjualan
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
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!,
padding: const EdgeInsets.all(16),
child: SalesRincianCard(state: salesState),
),
),
),
// Metode Pembayaran
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child:
BlocBuilder<
PaymentMethodAnalyticLoaderBloc,
PaymentMethodAnalyticLoaderState
>(
builder: (context, paymentState) {
return SalesPaymentMethodCard(
paymentMethodAnalytic:
paymentState.paymentMethodAnalytic,
isFetching: paymentState.isFetching,
);
},
),
),
),
),
// Penjualan per Kategori
SliverToBoxAdapter(
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(16),
child:
BlocBuilder<
CategoryAnalyticLoaderBloc,
CategoryAnalyticLoaderState
>(
builder: (context, categoryState) {
return SalesCategoryCard(
categoryAnalytic: categoryState.categoryAnalytic,
isFetching: categoryState.isFetching,
);
},
),
),
),
),
// Summary Cards
// Produk Terlaris
SliverToBoxAdapter(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.summary,
style: AppStyle.xxl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
const SpaceHeight(16),
state.isFetching
? _buildSummaryShimmer()
: _buildSummaryCards(state),
],
child:
BlocBuilder<
ProductAnalyticLoaderBloc,
ProductAnalyticLoaderState
>(
builder: (context, productState) {
return SalesTopProductsCard(
productAnalytic: productState.productAnalytic,
isFetching: productState.isFetching,
);
},
),
),
),
),
),
// 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(
context.lang.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)),
@ -200,559 +180,34 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
);
},
),
),
);
}
Widget _buildSummaryShimmer() {
return Column(
children: [
Row(
children: [
Expanded(child: _buildSummaryCardShimmer()),
SpaceWidth(12),
Expanded(child: _buildSummaryCardShimmer()),
],
),
const SpaceHeight(12),
Row(
children: [
Expanded(child: _buildSummaryCardShimmer()),
SpaceWidth(12),
Expanded(child: _buildSummaryCardShimmer()),
],
),
],
);
}
Widget _buildSummaryCardShimmer() {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
),
SpaceWidth(8),
Container(
width: 60,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(8),
Container(
width: double.infinity,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
);
}
Widget _buildNetSalesShimmer() {
return Container(
margin: const EdgeInsets.all(16),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(12),
),
),
SpaceWidth(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SpaceHeight(8),
Container(
width: 150,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
),
),
);
}
Widget _buildDailySalesShimmer() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(10),
),
),
SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 100,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SpaceHeight(4),
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
Container(
width: 60,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
],
),
),
),
);
},
childCount: 8, // Show 8 shimmer items while loading
),
);
}
Widget _buildSummaryCards(SalesLoaderState state) {
return Column(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.elasticOut,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Row(
children: [
Expanded(
child: _buildSummaryCard(
context.lang.total_sales,
state.sales.summary.totalSales.currencyFormatRp,
Icons.trending_up,
AppColor.success,
0,
),
),
SpaceWidth(12),
Expanded(
child: _buildSummaryCard(
context.lang.total_orders,
state.sales.summary.totalOrders.toString(),
Icons.shopping_cart,
AppColor.info,
100,
),
),
],
),
);
},
),
const SpaceHeight(12),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1000),
curve: Curves.elasticOut,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Row(
children: [
Expanded(
child: _buildSummaryCard(
context.lang.average_price,
state.sales.summary.averageOrderValue
.round()
.currencyFormatRp,
Icons.attach_money,
AppColor.warning,
200,
),
),
SpaceWidth(12),
Expanded(
child: _buildSummaryCard(
context.lang.total_items,
state.sales.summary.totalItems.toString(),
Icons.inventory,
AppColor.primary,
300,
),
),
],
),
);
},
),
],
);
}
Widget _buildNetSalesCard(SalesLoaderState state) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1200),
curve: Curves.bounceOut,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColor.successGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColor.success.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1500),
curve: Curves.elasticOut,
builder: (context, iconValue, child) {
return Transform.rotate(
angle: iconValue * 0.1,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.account_balance_wallet,
color: AppColor.textWhite,
size: 28,
),
),
);
},
),
SpaceWidth(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.net_sales,
style: TextStyle(
color: AppColor.textWhite.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SpaceHeight(4),
TweenAnimationBuilder<double>(
tween: Tween(
begin: 0.0,
end: state.sales.summary.netSales.toDouble(),
),
duration: const Duration(milliseconds: 2000),
curve: Curves.easeOutCubic,
builder: (context, countValue, child) {
return Text(
state.sales.summary.netSales.currencyFormatRp,
style: const TextStyle(
color: AppColor.textWhite,
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
],
),
),
);
},
);
}
Widget _buildDailySalesList(SalesLoaderState state) {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
// Calculate intervals ensuring they don't exceed 1.0
final slideStart = math.min(0.2 + (index * 0.05), 0.7);
final slideEnd = math.min(slideStart + 0.3, 1.0);
final fadeStart = math.min(0.3 + (index * 0.05), 0.8);
final fadeEnd = math.min(fadeStart + 0.2, 1.0);
return SlideTransition(
position:
Tween<Offset>(
begin: Offset(index.isEven ? -1.0 : 1.0, 0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: slideAnimationController,
curve: Interval(
slideStart,
slideEnd,
curve: Curves.easeOutBack,
),
),
),
child: FadeTransition(
opacity: Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: fadeAnimationController,
curve: Interval(fadeStart, fadeEnd, curve: Curves.easeOut),
),
),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: _buildDailySalesItem(state.sales.data[index]),
),
),
);
}, childCount: state.sales.data.length),
);
}
Widget _buildSummaryCard(
String title,
String value,
IconData icon,
Color color,
int delay,
void _onDateRangeChanged(
BuildContext context,
DateTime startDate,
DateTime endDate,
) {
return SalesSummaryCard(
fadeAnimation: fadeAnimation,
title: title,
value: value,
icon: icon,
color: color,
delay: delay,
);
}
// Update sales
context.read<SalesLoaderBloc>()
..add(SalesLoaderEvent.rangeDateChanged(startDate, endDate))
..add(SalesLoaderEvent.fectched());
Widget _buildDailySalesItem(SalesAnalyticData dailySale) {
return ExpansionTile(
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColor.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.calendar_today, color: AppColor.primary, size: 20),
),
title: Text(
'${dailySale.date.day}/${dailySale.date.month}/${dailySale.date.year}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
subtitle: Text(
dailySale.sales.currencyFormatRp,
style: TextStyle(
color: AppColor.success,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColor.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${dailySale.orders} ${context.lang.orders}',
style: TextStyle(
color: AppColor.info,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
),
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: _buildDetailItem(
context.lang.items,
'${dailySale.items}',
Icons.inventory_2,
),
),
Expanded(
child: _buildDetailItem(
context.lang.tax,
dailySale.tax.currencyFormatRp,
Icons.receipt,
),
),
Expanded(
child: _buildDetailItem(
context.lang.discount,
dailySale.discount.currencyFormatRp,
Icons.local_offer,
),
),
],
),
),
],
);
}
// Update payment method
context.read<PaymentMethodAnalyticLoaderBloc>()
..add(
PaymentMethodAnalyticLoaderEvent.rangeDateChanged(startDate, endDate),
)
..add(const PaymentMethodAnalyticLoaderEvent.fetched());
Widget _buildDetailItem(String label, String value, IconData icon) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.bounceOut,
builder: (context, animValue, child) {
return Transform.scale(
scale: animValue,
child: Column(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
curve: Curves.elasticOut,
builder: (context, iconValue, child) {
return Transform.rotate(
angle: iconValue * 0.1,
child: Icon(icon, color: AppColor.textSecondary, size: 20),
);
},
),
const SpaceHeight(4),
Text(
label,
style: TextStyle(color: AppColor.textSecondary, fontSize: 12),
),
const SpaceHeight(2),
AnimatedBuilder(
animation: fadeAnimation,
builder: (context, child) {
return Text(
value,
style: TextStyle(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
);
},
),
],
),
);
},
);
// Update category
context.read<CategoryAnalyticLoaderBloc>()
..add(CategoryAnalyticLoaderEvent.rangeDateChanged(startDate, endDate))
..add(const CategoryAnalyticLoaderEvent.fetched());
// Update product
context.read<ProductAnalyticLoaderBloc>()
..add(ProductAnalyticLoaderEvent.rangeDateChanged(startDate, endDate))
..add(const ProductAnalyticLoaderEvent.fetched());
}
}

View File

@ -0,0 +1,227 @@
import 'package:flutter/material.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
class ProductDetailBottomSheet {
static void show({
required BuildContext context,
required ProductAnalyticData product,
required int totalRevenue,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.transparent,
builder: (context) =>
_ProductDetailContent(product: product, totalRevenue: totalRevenue),
);
}
}
class _ProductDetailContent extends StatelessWidget {
final ProductAnalyticData product;
final int totalRevenue;
const _ProductDetailContent({
required this.product,
required this.totalRevenue,
});
@override
Widget build(BuildContext context) {
final contribution = totalRevenue > 0
? (product.revenue / totalRevenue * 100)
: 0.0;
final hppPercentage = product.revenue > 0
? (product.standardHppTotal / product.revenue * 100)
: 0.0;
final marginStatus = _getMarginStatus(hppPercentage);
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
// Product name
Text(
product.productName.toUpperCase(),
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
fontSize: 20,
),
),
const SpaceHeight(4),
// Category
Text(
product.categoryName,
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SpaceHeight(20),
// Details
_buildRow(context, 'Kategori', product.categoryName),
_buildDivider(),
_buildRow(
context,
'Qty ${context.lang.sold.toLowerCase()}',
'${product.quantitySold} porsi',
),
_buildDivider(),
_buildRow(
context,
context.lang.revenue,
product.revenue.currencyFormatRp,
),
_buildDivider(),
_buildRow(
context,
'Kontribusi omzet',
'${contribution.toStringAsFixed(1)}%',
),
_buildDivider(),
_buildRow(
context,
'% ${context.lang.hpp} (real)',
'${hppPercentage.toStringAsFixed(1)}%',
),
_buildDivider(),
_buildRowWithBadge(
context,
'Status margin',
marginStatus.label,
marginStatus.color,
),
const SpaceHeight(16),
],
),
),
);
}
Widget _buildRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
Text(
value,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
],
),
);
}
Widget _buildRowWithBadge(
BuildContext context,
String label,
String badgeText,
Color badgeColor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppStyle.md.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
badgeText,
style: AppStyle.sm.copyWith(
color: badgeColor,
fontWeight: FontWeight.w700,
fontSize: 12,
),
),
),
],
),
);
}
Widget _buildDivider() {
return Divider(
height: 1,
thickness: 0.5,
color: AppColor.border.withOpacity(0.5),
);
}
_MarginStatus _getMarginStatus(double hppPercentage) {
if (hppPercentage <= 50) {
return _MarginStatus('Sehat', AppColor.success);
} else {
return _MarginStatus('Tidak sehat', AppColor.error);
}
}
}
class _MarginStatus {
final String label;
final Color color;
_MarginStatus(this.label, this.color);
}

View File

@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
class SalesCategoryCard extends StatelessWidget {
final CategoryAnalytic categoryAnalytic;
final bool isFetching;
const SalesCategoryCard({
super.key,
required this.categoryAnalytic,
required this.isFetching,
});
@override
Widget build(BuildContext context) {
if (isFetching) return _buildShimmer();
final data = categoryAnalytic.data;
final totalItems = data.fold<int>(0, (sum, e) => sum + e.totalQuantity);
final totalRevenue = data.fold<int>(0, (sum, e) => sum + e.totalRevenue);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.sales_category,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
fontSize: 18,
),
),
Text(
'$totalItems ${context.lang.item.toLowerCase()}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
const SpaceHeight(16),
// Category items
if (data.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
context.lang.category_no_data,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
)
else
...data.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final percentage = totalRevenue > 0
? (item.totalRevenue / totalRevenue * 100)
: 0.0;
return Column(
children: [
_buildCategoryItem(item, percentage, index),
if (index < data.length - 1) const SpaceHeight(16),
],
);
}),
],
),
);
}
Widget _buildCategoryItem(
CategoryAnalyticItem item,
double percentage,
int index,
) {
final colors = [
AppColor.primary,
const Color(0xFF00BCD4),
const Color(0xFFFF9800),
const Color(0xFF9E9E9E),
AppColor.success,
AppColor.info,
];
final color = colors[index % colors.length];
return Column(
children: [
// Name + count + amount + percentage
Row(
children: [
Expanded(
child: Row(
children: [
Flexible(
child: Text(
item.categoryName,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
const SpaceWidth(8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColor.textSecondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${item.totalQuantity}x',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
),
Text(
item.totalRevenue.currencyFormatRp,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SpaceWidth(6),
Text(
'${percentage.toStringAsFixed(1)}%',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
const SpaceHeight(8),
// Progress bar
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: percentage / 100,
minHeight: 6,
backgroundColor: AppColor.border.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(color),
),
),
],
);
}
Widget _buildShimmer() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 160,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 60,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(20),
...List.generate(
4,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 120,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 90,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(8),
Container(
width: double.infinity,
height: 6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,315 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/sales_loader/sales_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/painter/wave_painter.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/bottom_sheet/date_range_bottom_sheet.dart';
import '../../../components/spacer/spacer.dart';
class SalesHeader extends StatelessWidget {
final SalesLoaderState state;
final void Function(DateTime startDate, DateTime endDate)? onDateRangeChanged;
const SalesHeader({super.key, required this.state, this.onDateRangeChanged});
@override
Widget build(BuildContext context) {
final dateLabel = _formatDateRange(state.dateFrom, state.dateTo, context);
final outletLabel = state.sales.outletName.isNotEmpty
? state.sales.outletName
: 'Semua Outlet';
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Stack(
children: [
// Decorative circles
Positioned(
top: -20,
right: -30,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.08),
),
),
),
Positioned(
top: 30,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.05),
),
),
),
Positioned(
top: 10,
left: -20,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.textWhite.withOpacity(0.04),
),
),
),
// Wave pattern
Positioned.fill(
child: CustomPaint(
painter: WavePainter(
animation: 0.0,
color: AppColor.textWhite.withOpacity(0.1),
),
),
),
// Content
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Title row + Calendar button
Row(
children: [
GestureDetector(
onTap: () => context.router.maybePop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.chevron_left_rounded,
color: AppColor.textWhite,
size: 24,
),
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.sales,
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
const SizedBox(height: 2),
Text(
'$dateLabel · $outletLabel',
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SpaceWidth(8),
// Date filter button
GestureDetector(
onTap: () => _showDatePicker(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.calendar_month_rounded,
color: AppColor.textWhite,
size: 20,
),
),
),
],
),
const SpaceHeight(24),
// Total Penjualan label
Text(
context.lang.total_sales_label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite.withOpacity(0.75),
fontWeight: FontWeight.w400,
fontSize: 13,
),
),
const SpaceHeight(4),
// Big value
state.isFetching
? _buildHeaderValueShimmer()
: Text(
state.sales.summary.totalSales.currencyFormatRp,
style: AppStyle.h1.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w900,
fontSize: 32,
),
),
const SpaceHeight(16),
// Chips row
state.isFetching
? _buildHeaderChipsShimmer()
: _buildHeaderChips(context),
],
),
),
),
],
),
);
}
void _showDatePicker(BuildContext context) {
DateRangePickerBottomSheet.show(
context: context,
primaryColor: AppColor.primary,
initialStartDate: state.dateFrom,
initialEndDate: state.dateTo,
maxDate: DateTime.now(),
onChanged: (startDate, endDate) {
if (startDate != null && endDate != null) {
onDateRangeChanged?.call(startDate, endDate);
}
},
);
}
Widget _buildHeaderValueShimmer() {
return Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.3),
highlightColor: AppColor.textWhite.withOpacity(0.6),
child: Container(
width: 200,
height: 36,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
);
}
Widget _buildHeaderChipsShimmer() {
return Row(
children: List.generate(
3,
(index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Shimmer.fromColors(
baseColor: AppColor.textWhite.withOpacity(0.15),
highlightColor: AppColor.textWhite.withOpacity(0.3),
child: Container(
width: 90,
height: 32,
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
),
),
),
),
);
}
Widget _buildHeaderChips(BuildContext context) {
final summary = state.sales.summary;
final avgPerInvoice = summary.totalOrders > 0
? (summary.totalSales / summary.totalOrders).round()
: 0;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('${summary.totalOrders} invoice'),
_buildChip(
'${summary.totalItems} ${context.lang.items_sold.toLowerCase()}',
),
_buildChip('${avgPerInvoice.currencyFormatRp}/invoice'),
],
);
}
Widget _buildChip(String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColor.textWhite.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColor.textWhite.withOpacity(0.25)),
),
child: Text(
label,
style: AppStyle.sm.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
);
}
String _formatDateRange(DateTime from, DateTime to, BuildContext context) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
if (from.year == to.year && from.month == to.month && from.day == to.day) {
return '${context.lang.report} ${from.day} ${months[from.month - 1]} ${from.year}';
}
return '${context.lang.report} ${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}';
}
}

View File

@ -0,0 +1,265 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
class SalesPaymentMethodCard extends StatelessWidget {
final PaymentMethodAnalytic paymentMethodAnalytic;
final bool isFetching;
const SalesPaymentMethodCard({
super.key,
required this.paymentMethodAnalytic,
required this.isFetching,
});
@override
Widget build(BuildContext context) {
if (isFetching) return _buildShimmer();
final data = paymentMethodAnalytic.data;
final totalTransactions = paymentMethodAnalytic.summary.totalOrders;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.payment_methods,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
fontSize: 18,
),
),
Text(
'$totalTransactions ${context.lang.transactions.toLowerCase()}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
const SpaceHeight(16),
// Payment method items
if (data.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
context.lang.no_data_available,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
)
else
...data.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Column(
children: [
_buildPaymentMethodItem(item, index),
if (index < data.length - 1) const SpaceHeight(16),
],
);
}),
],
),
);
}
Widget _buildPaymentMethodItem(PaymentMethodItem item, int index) {
final colors = [
AppColor.primary,
const Color(0xFF00BCD4),
const Color(0xFFFF9800),
AppColor.success,
AppColor.info,
];
final color = colors[index % colors.length];
return Column(
children: [
// Name + count + amount + percentage
Row(
children: [
// Name
Expanded(
child: Row(
children: [
Text(
item.paymentMethodName,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SpaceWidth(8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColor.textSecondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${item.paymentCount}x',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
),
// Amount
Text(
item.totalAmount.toInt().currencyFormatRp,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SpaceWidth(6),
// Percentage
Text(
'${item.percentage.toStringAsFixed(1)}%',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
const SpaceHeight(8),
// Progress bar
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: item.percentage / 100,
minHeight: 6,
backgroundColor: AppColor.border.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(color),
),
),
],
);
}
Widget _buildShimmer() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 150,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(20),
...List.generate(
3,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 120,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 90,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(8),
Container(
width: double.infinity,
height: 6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../application/analytic/sales_loader/sales_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
class SalesRincianCard extends StatelessWidget {
final SalesLoaderState state;
const SalesRincianCard({super.key, required this.state});
@override
Widget build(BuildContext context) {
if (state.isFetching) return _buildShimmer();
final summary = state.sales.summary;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.daily_breakdown,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
fontSize: 18,
),
),
const SpaceHeight(16),
_buildRow(
context.lang.gross_sales,
summary.totalSales.currencyFormatRp,
),
_buildDivider(),
_buildRow(
context.lang.discount,
summary.totalDiscount.currencyFormatRp,
),
_buildDivider(),
_buildRow(context.lang.tax, summary.totalTax.currencyFormatRp),
_buildDivider(),
_buildRow('Biaya layanan', 0.currencyFormatRp),
_buildDivider(),
_buildRow(
context.lang.net_sales,
summary.netSales.currencyFormatRp,
isBold: true,
),
],
),
);
}
Widget _buildRow(String label, String value, {bool isBold = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppStyle.md.copyWith(
color: isBold ? AppColor.textPrimary : AppColor.textSecondary,
fontWeight: isBold ? FontWeight.w700 : FontWeight.w500,
fontSize: 14,
),
),
Text(
value,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: isBold ? FontWeight.w700 : FontWeight.w600,
fontSize: 14,
),
),
],
),
);
}
Widget _buildDivider() {
return Divider(
height: 1,
thickness: 1,
color: AppColor.border.withOpacity(0.5),
);
}
Widget _buildShimmer() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 140,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SpaceHeight(20),
...List.generate(
5,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 100,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,288 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/analytic/analytic.dart';
import '../../../components/spacer/spacer.dart';
import 'product_detail_bottom_sheet.dart';
class SalesTopProductsCard extends StatelessWidget {
final ProductAnalytic productAnalytic;
final bool isFetching;
const SalesTopProductsCard({
super.key,
required this.productAnalytic,
required this.isFetching,
});
@override
Widget build(BuildContext context) {
if (isFetching) return _buildShimmer();
final data = productAnalytic.data;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.lang.best_selling_products,
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
fontSize: 18,
),
),
Text(
'${data.length} ${context.lang.product.toLowerCase()}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
const SpaceHeight(16),
// Product list
if (data.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
context.lang.no_data_available,
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
),
)
else
...data.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Column(
children: [
_buildProductItem(context, item, index + 1),
if (index < data.length - 1)
const Divider(height: 1, thickness: 0.5),
],
);
}),
],
),
);
}
Widget _buildProductItem(
BuildContext context,
ProductAnalyticData item,
int rank,
) {
final isTopThree = rank <= 3;
final totalRevenue = productAnalytic.data.fold<int>(
0,
(sum, e) => sum + e.revenue,
);
return GestureDetector(
onTap: () => ProductDetailBottomSheet.show(
context: context,
product: item,
totalRevenue: totalRevenue,
),
behavior: HitTestBehavior.opaque,
child: _buildProductItemContent(context, item, rank, isTopThree),
);
}
Widget _buildProductItemContent(
BuildContext context,
ProductAnalyticData item,
int rank,
bool isTopThree,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
// Rank badge
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isTopThree
? AppColor.primary.withOpacity(0.1)
: AppColor.textSecondary.withOpacity(0.08),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'$rank',
style: AppStyle.md.copyWith(
color: isTopThree ? AppColor.primary : AppColor.textSecondary,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
const SpaceWidth(12),
// Product name + sold count
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName.toUpperCase(),
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'${item.quantitySold} ${context.lang.sold.toLowerCase()}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
],
),
),
// Revenue
Text(
item.revenue.currencyFormatRp,
style: AppStyle.md.copyWith(
color: AppColor.textPrimary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
);
}
Widget _buildShimmer() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 140,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
Container(
width: 70,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
const SpaceHeight(20),
...List.generate(
6,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 150,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Container(
width: 60,
height: 12,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class SalesSummaryCard extends StatelessWidget {
const SalesSummaryCard({
super.key,
required this.fadeAnimation,
required this.title,
required this.value,
required this.icon,
required this.color,
required this.delay,
});
final Animation<double> fadeAnimation;
final String title;
final String value;
final IconData icon;
final Color color;
final int delay;
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 800 + delay),
curve: Curves.easeOutBack,
builder: (context, animValue, child) {
return Transform.scale(
scale: animValue,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 1000 + delay),
curve: Curves.bounceOut,
builder: (context, iconValue, child) {
return Transform.scale(
scale: iconValue,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
);
},
),
const Spacer(),
],
),
const SizedBox(height: 12),
Text(
title,
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
AnimatedBuilder(
animation: fadeAnimation,
builder: (context, child) {
return Text(
value,
style: AppStyle.xl.copyWith(
color: AppColor.textPrimary,
fontSize: 18,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
);
},
);
}
}