purchasing
This commit is contained in:
parent
602647ff26
commit
823e009121
@ -28,22 +28,23 @@ class PurchasingAnalyticLoaderBloc
|
||||
rangeDateChanged: (e) async {
|
||||
emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo));
|
||||
},
|
||||
outletChanged: (e) async {
|
||||
emit(state.copyWith(outletId: e.outletId));
|
||||
},
|
||||
fetched: (e) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isFetching: true,
|
||||
failureOptionPurchasing: none(),
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
isFetching: true,
|
||||
failureOptionPurchasing: none(),
|
||||
));
|
||||
|
||||
final result = await _analyticRepository.getPurchasing(
|
||||
dateFrom: state.dateFrom,
|
||||
dateTo: state.dateTo,
|
||||
outletId: state.outletId,
|
||||
);
|
||||
|
||||
final newState = result.fold(
|
||||
(f) =>
|
||||
state.copyWith(failureOptionPurchasing: optionOf(f)),
|
||||
(f) => state.copyWith(failureOptionPurchasing: optionOf(f)),
|
||||
(purchasing) => state.copyWith(purchasing: purchasing),
|
||||
);
|
||||
|
||||
|
||||
@ -21,32 +21,38 @@ mixin _$PurchasingAnalyticLoaderEvent {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime dateFrom, DateTime dateTo)
|
||||
rangeDateChanged,
|
||||
required TResult Function(String? outletId) outletChanged,
|
||||
required TResult Function() fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult? Function(String? outletId)? outletChanged,
|
||||
TResult? Function()? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult Function(String? outletId)? outletChanged,
|
||||
TResult Function()? fetched,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_RangeDateChanged value) rangeDateChanged,
|
||||
required TResult Function(_OutletChanged value) outletChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult? Function(_OutletChanged value)? outletChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult Function(_OutletChanged value)? outletChanged,
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@ -168,6 +174,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime dateFrom, DateTime dateTo)
|
||||
rangeDateChanged,
|
||||
required TResult Function(String? outletId) outletChanged,
|
||||
required TResult Function() fetched,
|
||||
}) {
|
||||
return rangeDateChanged(dateFrom, dateTo);
|
||||
@ -177,6 +184,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult? Function(String? outletId)? outletChanged,
|
||||
TResult? Function()? fetched,
|
||||
}) {
|
||||
return rangeDateChanged?.call(dateFrom, dateTo);
|
||||
@ -186,6 +194,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult Function(String? outletId)? outletChanged,
|
||||
TResult Function()? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -199,6 +208,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_RangeDateChanged value) rangeDateChanged,
|
||||
required TResult Function(_OutletChanged value) outletChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return rangeDateChanged(this);
|
||||
@ -208,6 +218,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult? Function(_OutletChanged value)? outletChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return rangeDateChanged?.call(this);
|
||||
@ -217,6 +228,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged {
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult Function(_OutletChanged value)? outletChanged,
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -243,6 +255,157 @@ abstract class _RangeDateChanged implements PurchasingAnalyticLoaderEvent {
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$OutletChangedImplCopyWith<$Res> {
|
||||
factory _$$OutletChangedImplCopyWith(
|
||||
_$OutletChangedImpl value,
|
||||
$Res Function(_$OutletChangedImpl) then,
|
||||
) = __$$OutletChangedImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({String? outletId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$OutletChangedImplCopyWithImpl<$Res>
|
||||
extends
|
||||
_$PurchasingAnalyticLoaderEventCopyWithImpl<$Res, _$OutletChangedImpl>
|
||||
implements _$$OutletChangedImplCopyWith<$Res> {
|
||||
__$$OutletChangedImplCopyWithImpl(
|
||||
_$OutletChangedImpl _value,
|
||||
$Res Function(_$OutletChangedImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? outletId = freezed}) {
|
||||
return _then(
|
||||
_$OutletChangedImpl(
|
||||
freezed == outletId
|
||||
? _value.outletId
|
||||
: outletId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$OutletChangedImpl implements _OutletChanged {
|
||||
const _$OutletChangedImpl(this.outletId);
|
||||
|
||||
@override
|
||||
final String? outletId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PurchasingAnalyticLoaderEvent.outletChanged(outletId: $outletId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$OutletChangedImpl &&
|
||||
(identical(other.outletId, outletId) ||
|
||||
other.outletId == outletId));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, outletId);
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$OutletChangedImplCopyWith<_$OutletChangedImpl> get copyWith =>
|
||||
__$$OutletChangedImplCopyWithImpl<_$OutletChangedImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime dateFrom, DateTime dateTo)
|
||||
rangeDateChanged,
|
||||
required TResult Function(String? outletId) outletChanged,
|
||||
required TResult Function() fetched,
|
||||
}) {
|
||||
return outletChanged(outletId);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult? Function(String? outletId)? outletChanged,
|
||||
TResult? Function()? fetched,
|
||||
}) {
|
||||
return outletChanged?.call(outletId);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult Function(String? outletId)? outletChanged,
|
||||
TResult Function()? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (outletChanged != null) {
|
||||
return outletChanged(outletId);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_RangeDateChanged value) rangeDateChanged,
|
||||
required TResult Function(_OutletChanged value) outletChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return outletChanged(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult? Function(_OutletChanged value)? outletChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return outletChanged?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult Function(_OutletChanged value)? outletChanged,
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (outletChanged != null) {
|
||||
return outletChanged(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _OutletChanged implements PurchasingAnalyticLoaderEvent {
|
||||
const factory _OutletChanged(final String? outletId) = _$OutletChangedImpl;
|
||||
|
||||
String? get outletId;
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$OutletChangedImplCopyWith<_$OutletChangedImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$FetchedImplCopyWith<$Res> {
|
||||
factory _$$FetchedImplCopyWith(
|
||||
@ -288,6 +451,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime dateFrom, DateTime dateTo)
|
||||
rangeDateChanged,
|
||||
required TResult Function(String? outletId) outletChanged,
|
||||
required TResult Function() fetched,
|
||||
}) {
|
||||
return fetched();
|
||||
@ -297,6 +461,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult? Function(String? outletId)? outletChanged,
|
||||
TResult? Function()? fetched,
|
||||
}) {
|
||||
return fetched?.call();
|
||||
@ -306,6 +471,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
|
||||
TResult Function(String? outletId)? outletChanged,
|
||||
TResult Function()? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -319,6 +485,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_RangeDateChanged value) rangeDateChanged,
|
||||
required TResult Function(_OutletChanged value) outletChanged,
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return fetched(this);
|
||||
@ -328,6 +495,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult? Function(_OutletChanged value)? outletChanged,
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return fetched?.call(this);
|
||||
@ -337,6 +505,7 @@ class _$FetchedImpl implements _Fetched {
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_RangeDateChanged value)? rangeDateChanged,
|
||||
TResult Function(_OutletChanged value)? outletChanged,
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -359,6 +528,7 @@ mixin _$PurchasingAnalyticLoaderState {
|
||||
bool get isFetching => throw _privateConstructorUsedError;
|
||||
DateTime get dateFrom => throw _privateConstructorUsedError;
|
||||
DateTime get dateTo => throw _privateConstructorUsedError;
|
||||
String? get outletId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -384,6 +554,7 @@ abstract class $PurchasingAnalyticLoaderStateCopyWith<$Res> {
|
||||
bool isFetching,
|
||||
DateTime dateFrom,
|
||||
DateTime dateTo,
|
||||
String? outletId,
|
||||
});
|
||||
|
||||
$PurchasingAnalyticCopyWith<$Res> get purchasing;
|
||||
@ -412,6 +583,7 @@ class _$PurchasingAnalyticLoaderStateCopyWithImpl<
|
||||
Object? isFetching = null,
|
||||
Object? dateFrom = null,
|
||||
Object? dateTo = null,
|
||||
Object? outletId = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@ -435,6 +607,10 @@ class _$PurchasingAnalyticLoaderStateCopyWithImpl<
|
||||
? _value.dateTo
|
||||
: dateTo // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
outletId: freezed == outletId
|
||||
? _value.outletId
|
||||
: outletId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@ -466,6 +642,7 @@ abstract class _$$PurchasingAnalyticLoaderStateImplCopyWith<$Res>
|
||||
bool isFetching,
|
||||
DateTime dateFrom,
|
||||
DateTime dateTo,
|
||||
String? outletId,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -495,6 +672,7 @@ class __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>
|
||||
Object? isFetching = null,
|
||||
Object? dateFrom = null,
|
||||
Object? dateTo = null,
|
||||
Object? outletId = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$PurchasingAnalyticLoaderStateImpl(
|
||||
@ -518,6 +696,10 @@ class __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>
|
||||
? _value.dateTo
|
||||
: dateTo // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
outletId: freezed == outletId
|
||||
? _value.outletId
|
||||
: outletId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -533,6 +715,7 @@ class _$PurchasingAnalyticLoaderStateImpl
|
||||
this.isFetching = false,
|
||||
required this.dateFrom,
|
||||
required this.dateTo,
|
||||
this.outletId,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -546,10 +729,12 @@ class _$PurchasingAnalyticLoaderStateImpl
|
||||
final DateTime dateFrom;
|
||||
@override
|
||||
final DateTime dateTo;
|
||||
@override
|
||||
final String? outletId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PurchasingAnalyticLoaderState(purchasing: $purchasing, failureOptionPurchasing: $failureOptionPurchasing, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)';
|
||||
return 'PurchasingAnalyticLoaderState(purchasing: $purchasing, failureOptionPurchasing: $failureOptionPurchasing, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo, outletId: $outletId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -568,7 +753,9 @@ class _$PurchasingAnalyticLoaderStateImpl
|
||||
other.isFetching == isFetching) &&
|
||||
(identical(other.dateFrom, dateFrom) ||
|
||||
other.dateFrom == dateFrom) &&
|
||||
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
|
||||
(identical(other.dateTo, dateTo) || other.dateTo == dateTo) &&
|
||||
(identical(other.outletId, outletId) ||
|
||||
other.outletId == outletId));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -579,6 +766,7 @@ class _$PurchasingAnalyticLoaderStateImpl
|
||||
isFetching,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
outletId,
|
||||
);
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderState
|
||||
@ -603,6 +791,7 @@ abstract class _PurchasingAnalyticLoaderState
|
||||
final bool isFetching,
|
||||
required final DateTime dateFrom,
|
||||
required final DateTime dateTo,
|
||||
final String? outletId,
|
||||
}) = _$PurchasingAnalyticLoaderStateImpl;
|
||||
|
||||
@override
|
||||
@ -615,6 +804,8 @@ abstract class _PurchasingAnalyticLoaderState
|
||||
DateTime get dateFrom;
|
||||
@override
|
||||
DateTime get dateTo;
|
||||
@override
|
||||
String? get outletId;
|
||||
|
||||
/// Create a copy of PurchasingAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
@ -7,5 +7,9 @@ class PurchasingAnalyticLoaderEvent with _$PurchasingAnalyticLoaderEvent {
|
||||
DateTime dateTo,
|
||||
) = _RangeDateChanged;
|
||||
|
||||
const factory PurchasingAnalyticLoaderEvent.outletChanged(
|
||||
String? outletId,
|
||||
) = _OutletChanged;
|
||||
|
||||
const factory PurchasingAnalyticLoaderEvent.fetched() = _Fetched;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ class PurchasingAnalyticLoaderState with _$PurchasingAnalyticLoaderState {
|
||||
@Default(false) bool isFetching,
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
String? outletId,
|
||||
}) = _PurchasingAnalyticLoaderState;
|
||||
|
||||
factory PurchasingAnalyticLoaderState.initial() =>
|
||||
|
||||
@ -65,5 +65,8 @@ class ThemeApp {
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColor.white),
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: AppColor.white,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,51 +1,70 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../../../application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart';
|
||||
import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/appbar/appbar.dart';
|
||||
import 'widgets/purchase_tile.dart';
|
||||
import '../../components/field/date_range_picker_field.dart';
|
||||
import '../../components/spacer/spacer.dart';
|
||||
import 'widgets/ingredient_card.dart';
|
||||
import 'widgets/outlet_selector_field.dart';
|
||||
import 'widgets/purchase_daily_tile.dart';
|
||||
import 'widgets/stat_card.dart';
|
||||
import 'widgets/status_chip.dart';
|
||||
import 'widgets/vendor_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PurchasePage extends StatefulWidget {
|
||||
class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const PurchasePage({super.key});
|
||||
|
||||
@override
|
||||
State<PurchasePage> createState() => _PurchasePageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => getIt<PurchasingAnalyticLoaderBloc>()
|
||||
..add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => getIt<OutletListLoaderBloc>()
|
||||
..add(const OutletListLoaderEvent.fetched()),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController cardAnimation;
|
||||
String selectedFilter = 'Semua';
|
||||
final List<String> filterOptions = [
|
||||
'Semua',
|
||||
'Pending',
|
||||
'Completed',
|
||||
'Cancelled',
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> purchaseData = [
|
||||
];
|
||||
late AnimationController _fadeController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
cardAnimation = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
cardAnimation.forward();
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _fadeController,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (mounted) _fadeController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cardAnimation.dispose();
|
||||
_fadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -53,141 +72,521 @@ class _PurchasePageState extends State<PurchasePage>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColor.primary,
|
||||
|
||||
flexibleSpace: CustomAppBar(title: context.lang.purchase),
|
||||
body: MultiBlocListener(
|
||||
listeners: [
|
||||
// Re-fetch when date range changes
|
||||
BlocListener<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
prev.dateFrom != curr.dateFrom ||
|
||||
prev.dateTo != curr.dateTo,
|
||||
listener: (context, _) => context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
|
||||
// Stats Cards
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: AppColor.background,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: context.lang.total_purchase,
|
||||
value: 'Rp 0',
|
||||
icon: LineIcons.shoppingCart,
|
||||
iconColor: AppColor.success,
|
||||
cardAnimation: cardAnimation,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: context.lang.pending_order,
|
||||
value: '0 ${context.lang.orders}',
|
||||
icon: LineIcons.clock,
|
||||
iconColor: AppColor.warning,
|
||||
cardAnimation: cardAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Re-fetch when outlet changes
|
||||
BlocListener<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
listenWhen: (prev, curr) => prev.outletId != curr.outletId,
|
||||
listener: (context, _) => context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(const PurchasingAnalyticLoaderEvent.fetched()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
|
||||
builder: (context, outletListState) {
|
||||
return BlocBuilder<PurchasingAnalyticLoaderBloc,
|
||||
PurchasingAnalyticLoaderState>(
|
||||
builder: (context, state) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColor.primary,
|
||||
flexibleSpace:
|
||||
CustomAppBar(title: context.lang.purchase),
|
||||
),
|
||||
|
||||
// Filter Section
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: AppColor.surface,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.history_purchase,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
// Date Range + Outlet Picker
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
DateRangePickerField(
|
||||
maxDate: DateTime.now(),
|
||||
startDate: state.dateFrom,
|
||||
endDate: state.dateTo,
|
||||
onChanged: (startDate, endDate) {
|
||||
context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(
|
||||
PurchasingAnalyticLoaderEvent
|
||||
.rangeDateChanged(
|
||||
startDate!,
|
||||
endDate!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SpaceHeight(8),
|
||||
PurchaseOutletSelectorField(
|
||||
selectedOutletId: state.outletId,
|
||||
outlets: outletListState.outlets,
|
||||
isLoading: outletListState.isFetching,
|
||||
onOutletChanged: (outletId) {
|
||||
context
|
||||
.read<PurchasingAnalyticLoaderBloc>()
|
||||
.add(
|
||||
PurchasingAnalyticLoaderEvent
|
||||
.outletChanged(outletId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${purchaseData.length} ${context.lang.orders}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(16)),
|
||||
|
||||
// Summary Section
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Total Purchases Highlight Card
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: state.isFetching
|
||||
? _buildHighlightShimmer()
|
||||
: _buildTotalPurchasesCard(state),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Daily Breakdown Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
context.lang.daily_breakdown,
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildDailyList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Ingredient Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Bahan Baku',
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildIngredientList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(24)),
|
||||
|
||||
// Vendor Header
|
||||
SliverToBoxAdapter(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Vendor',
|
||||
style: AppStyle.xxl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SpaceHeight(12)),
|
||||
|
||||
state.isFetching
|
||||
? _buildListShimmer()
|
||||
: _buildVendorList(state),
|
||||
|
||||
const SliverToBoxAdapter(child: SpaceHeight(80)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Summary Cards ────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSummaryCards(PurchasingAnalyticLoaderState state) {
|
||||
final s = state.purchasing.summary;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total Pembelian',
|
||||
value: s.totalPurchases.currencyFormatRp,
|
||||
icon: LineIcons.shoppingCart,
|
||||
iconColor: AppColor.primary,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total PO',
|
||||
value: '${s.totalPurchaseOrders} PO',
|
||||
icon: LineIcons.fileAlt,
|
||||
iconColor: AppColor.info,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Total Qty',
|
||||
value: '${s.totalQuantity} pcs',
|
||||
icon: LineIcons.boxes,
|
||||
iconColor: AppColor.warning,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Rata-rata PO',
|
||||
value: s.averagePurchaseOrderValue.round().currencyFormatRp,
|
||||
icon: LineIcons.dollarSign,
|
||||
iconColor: AppColor.secondary,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Bahan Baku',
|
||||
value: '${s.totalIngredients} item',
|
||||
icon: LineIcons.leaf,
|
||||
iconColor: AppColor.secondaryDark,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(12),
|
||||
Expanded(
|
||||
child: PurchaseStatCard(
|
||||
title: 'Vendor',
|
||||
value: '${s.totalVendors} vendor',
|
||||
icon: LineIcons.truck,
|
||||
iconColor: AppColor.primaryDark,
|
||||
cardAnimation: _fadeAnimation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalPurchasesCard(PurchasingAnalyticLoaderState state) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 900),
|
||||
curve: Curves.bounceOut,
|
||||
builder: (context, value, _) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.35),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
LineIcons.shoppingBag,
|
||||
color: AppColor.textWhite,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.total_purchase,
|
||||
style: TextStyle(
|
||||
color: AppColor.textWhite.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(4),
|
||||
Text(
|
||||
state.purchasing.summary.totalPurchases
|
||||
.currencyFormatRp,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textWhite,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: filterOptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected =
|
||||
selectedFilter == filterOptions[index];
|
||||
return PurchaseStatusChip(
|
||||
isSelected: isSelected,
|
||||
text: filterOptions[index],
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
selectedFilter = filterOptions[index];
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Purchase List
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final purchase = purchaseData[index];
|
||||
return AnimatedBuilder(
|
||||
animation: cardAnimation,
|
||||
builder: (context, child) {
|
||||
final delay = index * 0.1;
|
||||
final animValue = (cardAnimation.value - delay).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 30 * (1 - animValue)),
|
||||
child: Opacity(
|
||||
opacity: animValue,
|
||||
child: PurchaseTile(purchase: purchase, index: index),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${state.purchasing.summary.totalPurchaseOrders} PO',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textWhite,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}, childCount: purchaseData.length),
|
||||
),
|
||||
Text(
|
||||
'purchase order',
|
||||
style: TextStyle(
|
||||
color: AppColor.textWhite.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom spacing for FAB
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 80)),
|
||||
],
|
||||
// ─── Lists ────────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildDailyList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.data.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data harian'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseDailyTile(
|
||||
data: state.purchasing.data[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.data.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIngredientList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.ingredientData.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data bahan baku'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseIngredientCard(
|
||||
data: state.purchasing.ingredientData[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.ingredientData.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVendorList(PurchasingAnalyticLoaderState state) {
|
||||
if (state.purchasing.vendorData.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: _buildEmptyState('Tidak ada data vendor'));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => PurchaseVendorCard(
|
||||
data: state.purchasing.vendorData[index],
|
||||
index: index,
|
||||
animation: _fadeAnimation,
|
||||
),
|
||||
childCount: state.purchasing.vendorData.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Empty State ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildEmptyState(String message) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
message,
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shimmer Loaders ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSummaryShimmer() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
const SpaceWidth(12),
|
||||
Expanded(child: _shimmerCard(height: 100)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHighlightShimmer() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _shimmerCard(height: 88),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListShimmer() {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, __) => Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: _shimmerCard(height: 72),
|
||||
),
|
||||
childCount: 4,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _shimmerCard({required double height}) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
160
lib/presentation/pages/purchase/widgets/ingredient_card.dart
Normal file
160
lib/presentation/pages/purchase/widgets/ingredient_card.dart
Normal file
@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseIngredientCard extends StatelessWidget {
|
||||
final PurchasingIngredientData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseIngredientCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Ingredient icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.leaf,
|
||||
color: AppColor.secondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Name & details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.ingredientName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_Chip(
|
||||
label: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.purchaseOrderCount} PO',
|
||||
color: AppColor.info,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Cost column
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
data.totalCost.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'@ ${data.averageUnitCost.round().currencyFormatRp}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
const _Chip({required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/outlet/outlet.dart';
|
||||
|
||||
/// Outlet selector field — styled like DateRangePickerField.
|
||||
/// Opens a bottom sheet to pick an outlet or "Semua Outlet".
|
||||
class PurchaseOutletSelectorField extends StatefulWidget {
|
||||
final String? selectedOutletId;
|
||||
final List<Outlet> outlets;
|
||||
final bool isLoading;
|
||||
final ValueChanged<String?> onOutletChanged;
|
||||
|
||||
const PurchaseOutletSelectorField({
|
||||
super.key,
|
||||
required this.selectedOutletId,
|
||||
required this.outlets,
|
||||
required this.isLoading,
|
||||
required this.onOutletChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PurchaseOutletSelectorField> createState() =>
|
||||
_PurchaseOutletSelectorFieldState();
|
||||
}
|
||||
|
||||
class _PurchaseOutletSelectorFieldState
|
||||
extends State<PurchaseOutletSelectorField> {
|
||||
bool _isPressed = false;
|
||||
|
||||
Outlet? get _selectedOutlet => widget.outlets
|
||||
.where((o) => o.id == widget.selectedOutletId)
|
||||
.firstOrNull;
|
||||
|
||||
String get _label => _selectedOutlet?.name ?? 'Semua Outlet';
|
||||
|
||||
bool get _hasValue => widget.selectedOutletId != null;
|
||||
|
||||
void _showSheet() {
|
||||
if (widget.isLoading) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _OutletBottomSheet(
|
||||
outlets: widget.outlets,
|
||||
selectedOutletId: widget.selectedOutletId,
|
||||
onSelected: (outletId) {
|
||||
Navigator.pop(context);
|
||||
widget.onOutletChanged(outletId);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _showSheet,
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
onTapCancel: () => setState(() => _isPressed = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _isPressed ? AppColor.backgroundLight : AppColor.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _isPressed ? AppColor.primary : AppColor.border,
|
||||
width: _isPressed ? 2 : 1,
|
||||
),
|
||||
boxShadow: _isPressed
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight:
|
||||
_hasValue ? FontWeight.w500 : FontWeight.w400,
|
||||
color: _hasValue
|
||||
? AppColor.textPrimary
|
||||
: AppColor.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: widget.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.store_rounded,
|
||||
size: 20,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Bottom Sheet ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _OutletBottomSheet extends StatelessWidget {
|
||||
final List<Outlet> outlets;
|
||||
final String? selectedOutletId;
|
||||
final ValueChanged<String?> onSelected;
|
||||
|
||||
const _OutletBottomSheet({
|
||||
required this.outlets,
|
||||
required this.selectedOutletId,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Handle bar
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Pilih Outlet',
|
||||
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// "Semua Outlet" option
|
||||
_OutletItem(
|
||||
label: 'Semua Outlet',
|
||||
icon: Icons.store_rounded,
|
||||
isSelected: selectedOutletId == null,
|
||||
onTap: () => onSelected(null),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Individual outlets
|
||||
...outlets.map(
|
||||
(outlet) => Column(
|
||||
children: [
|
||||
_OutletItem(
|
||||
label: outlet.name,
|
||||
icon: Icons.storefront_rounded,
|
||||
isSelected: selectedOutletId == outlet.id,
|
||||
isActive: outlet.isActive,
|
||||
onTap: () => onSelected(outlet.id),
|
||||
),
|
||||
if (outlet != outlets.last) const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── List Item ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _OutletItem extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool isSelected;
|
||||
final bool? isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _OutletItem({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
this.isActive,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primary.withOpacity(0.1)
|
||||
: AppColor.background,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isSelected ? AppColor.primary : AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
label,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: isSelected ? AppColor.primary : AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isActive != null) ...[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isActive! ? AppColor.success : AppColor.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_rounded, color: AppColor.primary, size: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/presentation/pages/purchase/widgets/purchase_daily_tile.dart
Normal file
165
lib/presentation/pages/purchase/widgets/purchase_daily_tile.dart
Normal file
@ -0,0 +1,165 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseDailyTile extends StatelessWidget {
|
||||
final PurchasingAnalyticData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseDailyTile({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.calendar,
|
||||
color: AppColor.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
data.date.toDate,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
data.purchases.currencyFormatRp,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${data.purchaseOrders} PO',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.info,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.boxes,
|
||||
label: 'Qty',
|
||||
value: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.leaf,
|
||||
label: 'Bahan',
|
||||
value: '${data.ingredients} item',
|
||||
color: AppColor.secondary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _DetailItem(
|
||||
icon: LineIcons.truck,
|
||||
label: 'Vendor',
|
||||
value: '${data.vendors}',
|
||||
color: AppColor.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
|
||||
const _DetailItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 18),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,314 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
|
||||
class PurchaseTile extends StatelessWidget {
|
||||
final Map<String, dynamic> purchase;
|
||||
final int index;
|
||||
const PurchaseTile({super.key, required this.purchase, required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color statusColor;
|
||||
|
||||
switch (purchase['status']) {
|
||||
case 'Completed':
|
||||
statusColor = AppColor.success;
|
||||
break;
|
||||
case 'Pending':
|
||||
statusColor = AppColor.warning;
|
||||
break;
|
||||
case 'Cancelled':
|
||||
statusColor = AppColor.error;
|
||||
break;
|
||||
default:
|
||||
statusColor = AppColor.textSecondary;
|
||||
}
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300 + (index * 50)),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Material(
|
||||
elevation: 0,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.surface, AppColor.surface.withOpacity(0.95)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.08),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
purchase['id'],
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
statusColor.withOpacity(0.15),
|
||||
statusColor.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: statusColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
purchase['status'],
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.background.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.building,
|
||||
color: AppColor.primary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
purchase['supplier'],
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.calendar,
|
||||
color: AppColor.info,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
purchase['date'],
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LineIcons.shoppingBag,
|
||||
color: AppColor.secondary,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${purchase['items']} ${context.lang.items}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.secondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.total_purchase,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${purchase['total'].toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildActionButton(LineIcons.eye, AppColor.info, () {}),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
LineIcons.edit,
|
||||
AppColor.warning,
|
||||
() {},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
LineIcons.trash,
|
||||
AppColor.error,
|
||||
() {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(
|
||||
IconData icon,
|
||||
Color color,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [color.withOpacity(0.15), color.withOpacity(0.05)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: onPressed,
|
||||
child: Center(child: Icon(icon, color: color, size: 18)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final Animation<double> cardAnimation;
|
||||
|
||||
const PurchaseStatCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
@ -25,65 +26,41 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
return Transform.scale(
|
||||
scale: 0.8 + (cardAnimation.value * 0.2),
|
||||
child: Opacity(
|
||||
opacity: cardAnimation.value,
|
||||
opacity: cardAnimation.value.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.surface, AppColor.surface.withOpacity(0.9)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
color: AppColor.primary.withOpacity(0.08),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
color: AppColor.black.withOpacity(0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: AppColor.border.withOpacity(0.3),
|
||||
color: AppColor.border.withOpacity(0.25),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
iconColor.withOpacity(0.15),
|
||||
iconColor.withOpacity(0.05),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 22),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.sm.copyWith(
|
||||
@ -91,14 +68,16 @@ class PurchaseStatCard extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.lg.copyWith(
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.textPrimary,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
|
||||
class PurchaseStatusChip extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final String text;
|
||||
final Function(bool) onSelected;
|
||||
const PurchaseStatusChip({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
required this.text,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(
|
||||
text,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: isSelected ? AppColor.textWhite : AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColor.backgroundLight,
|
||||
selectedColor: AppColor.primary,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
153
lib/presentation/pages/purchase/widgets/vendor_card.dart
Normal file
153
lib/presentation/pages/purchase/widgets/vendor_card.dart
Normal file
@ -0,0 +1,153 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import '../../../../common/extension/extension.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class PurchaseVendorCard extends StatelessWidget {
|
||||
final PurchasingVendorData data;
|
||||
final int index;
|
||||
final Animation<double> animation;
|
||||
|
||||
const PurchaseVendorCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.index,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.border.withOpacity(0.25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColor.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Vendor icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
LineIcons.truck,
|
||||
color: AppColor.info,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Name & details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.vendorName.trim(),
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_Chip(
|
||||
label: '${data.purchaseOrderCount} PO',
|
||||
color: AppColor.info,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.ingredientCount} bahan',
|
||||
color: AppColor.secondary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_Chip(
|
||||
label: '${data.quantity} pcs',
|
||||
color: AppColor.warning,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Total cost
|
||||
Text(
|
||||
data.totalCost.currencyFormatRp,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
const _Chip({required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -501,7 +501,7 @@ class PurchaseRoute extends _i26.PageRouteInfo<void> {
|
||||
static _i26.PageInfo page = _i26.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i21.PurchasePage();
|
||||
return _i26.WrappedRoute(child: const _i21.PurchasePage());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
38
pubspec.lock
38
pubspec.lock
@ -213,10 +213,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -865,26 +865,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
line_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -921,26 +921,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1510,10 +1510,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.10"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1646,10 +1646,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1731,5 +1731,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
dart: ">=3.9.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@ -3,7 +3,7 @@ description: "A new Flutter project."
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.0.0+1
|
||||
version: 1.0.1+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user