diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..3181671 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,47 @@ +name: Build Android APK (split per ABI) + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build-apk: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + cache: "gradle" + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + # Samain dengan versi di ios.yml biar konsisten. + flutter-version: "3.47.0" + cache: true + + - name: Install Dependencies + run: flutter pub get + + # ── Build ─────────────────────────────────────────────────────────────── + + - name: Build APK (split per ABI) + run: flutter build apk --release --split-per-abi + + # ── Artifact ──────────────────────────────────────────────────────────── + + - name: Upload APKs as Artifact + uses: actions/upload-artifact@v4 + with: + name: android-apk + path: build/app/outputs/flutter-apk/app-*-release.apk + retention-days: 7 diff --git a/lib/application/order/order_loader/order_loader_bloc.dart b/lib/application/order/order_loader/order_loader_bloc.dart index 6f39707..95f773b 100644 --- a/lib/application/order/order_loader/order_loader_bloc.dart +++ b/lib/application/order/order_loader/order_loader_bloc.dart @@ -46,6 +46,31 @@ class OrderLoaderBloc extends Bloc { emit(newState); }, + summaryFetched: (e) async { + emit(state.copyWith(isFetchingSummary: true)); + + final results = await Future.wait([ + _repository.getTotalCount( + outletId: state.outletId, + dateFrom: state.dateFrom, + dateTo: state.dateTo, + ), + _repository.getTotalCount( + status: 'pending', + outletId: state.outletId, + dateFrom: state.dateFrom, + dateTo: state.dateTo, + ), + ]); + + emit( + state.copyWith( + isFetchingSummary: false, + totalOrder: results[0].getOrElse(() => 0), + totalPendingOrder: results[1].getOrElse(() => 0), + ), + ); + }, ); } diff --git a/lib/application/order/order_loader/order_loader_bloc.freezed.dart b/lib/application/order/order_loader/order_loader_bloc.freezed.dart index 5ac54a8..285cf2b 100644 --- a/lib/application/order/order_loader/order_loader_bloc.freezed.dart +++ b/lib/application/order/order_loader/order_loader_bloc.freezed.dart @@ -25,6 +25,7 @@ mixin _$OrderLoaderEvent { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ @@ -33,6 +34,7 @@ mixin _$OrderLoaderEvent { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ @@ -41,6 +43,7 @@ mixin _$OrderLoaderEvent { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -50,6 +53,7 @@ mixin _$OrderLoaderEvent { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ @@ -58,6 +62,7 @@ mixin _$OrderLoaderEvent { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ @@ -66,6 +71,7 @@ mixin _$OrderLoaderEvent { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) => throw _privateConstructorUsedError; } @@ -179,6 +185,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) { return rangeDateChanged(dateFrom, dateTo); } @@ -191,6 +198,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) { return rangeDateChanged?.call(dateFrom, dateTo); } @@ -203,6 +211,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) { if (rangeDateChanged != null) { @@ -219,6 +228,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) { return rangeDateChanged(this); } @@ -231,6 +241,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) { return rangeDateChanged?.call(this); } @@ -243,6 +254,7 @@ class _$RangeDateChangedImpl implements _RangeDateChanged { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) { if (rangeDateChanged != null) { @@ -344,6 +356,7 @@ class _$StatusChangedImpl implements _StatusChanged { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) { return statusChanged(status); } @@ -356,6 +369,7 @@ class _$StatusChangedImpl implements _StatusChanged { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) { return statusChanged?.call(status); } @@ -368,6 +382,7 @@ class _$StatusChangedImpl implements _StatusChanged { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) { if (statusChanged != null) { @@ -384,6 +399,7 @@ class _$StatusChangedImpl implements _StatusChanged { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) { return statusChanged(this); } @@ -396,6 +412,7 @@ class _$StatusChangedImpl implements _StatusChanged { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) { return statusChanged?.call(this); } @@ -408,6 +425,7 @@ class _$StatusChangedImpl implements _StatusChanged { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) { if (statusChanged != null) { @@ -505,6 +523,7 @@ class _$SearchChangedImpl implements _SearchChanged { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) { return searchChanged(search); } @@ -517,6 +536,7 @@ class _$SearchChangedImpl implements _SearchChanged { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) { return searchChanged?.call(search); } @@ -529,6 +549,7 @@ class _$SearchChangedImpl implements _SearchChanged { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) { if (searchChanged != null) { @@ -545,6 +566,7 @@ class _$SearchChangedImpl implements _SearchChanged { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) { return searchChanged(this); } @@ -557,6 +579,7 @@ class _$SearchChangedImpl implements _SearchChanged { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) { return searchChanged?.call(this); } @@ -569,6 +592,7 @@ class _$SearchChangedImpl implements _SearchChanged { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) { if (searchChanged != null) { @@ -667,6 +691,7 @@ class _$OutletChangedImpl implements _OutletChanged { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) { return outletChanged(outletId); } @@ -679,6 +704,7 @@ class _$OutletChangedImpl implements _OutletChanged { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) { return outletChanged?.call(outletId); } @@ -691,6 +717,7 @@ class _$OutletChangedImpl implements _OutletChanged { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) { if (outletChanged != null) { @@ -707,6 +734,7 @@ class _$OutletChangedImpl implements _OutletChanged { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) { return outletChanged(this); } @@ -719,6 +747,7 @@ class _$OutletChangedImpl implements _OutletChanged { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) { return outletChanged?.call(this); } @@ -731,6 +760,7 @@ class _$OutletChangedImpl implements _OutletChanged { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) { if (outletChanged != null) { @@ -830,6 +860,7 @@ class _$FetchedImpl implements _Fetched { required TResult Function(String search) searchChanged, required TResult Function(String? outletId) outletChanged, required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, }) { return fetched(isRefresh); } @@ -842,6 +873,7 @@ class _$FetchedImpl implements _Fetched { TResult? Function(String search)? searchChanged, TResult? Function(String? outletId)? outletChanged, TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, }) { return fetched?.call(isRefresh); } @@ -854,6 +886,7 @@ class _$FetchedImpl implements _Fetched { TResult Function(String search)? searchChanged, TResult Function(String? outletId)? outletChanged, TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, required TResult orElse(), }) { if (fetched != null) { @@ -870,6 +903,7 @@ class _$FetchedImpl implements _Fetched { required TResult Function(_SearchChanged value) searchChanged, required TResult Function(_OutletChanged value) outletChanged, required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, }) { return fetched(this); } @@ -882,6 +916,7 @@ class _$FetchedImpl implements _Fetched { TResult? Function(_SearchChanged value)? searchChanged, TResult? Function(_OutletChanged value)? outletChanged, TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, }) { return fetched?.call(this); } @@ -894,6 +929,7 @@ class _$FetchedImpl implements _Fetched { TResult Function(_SearchChanged value)? searchChanged, TResult Function(_OutletChanged value)? outletChanged, TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, required TResult orElse(), }) { if (fetched != null) { @@ -915,6 +951,138 @@ abstract class _Fetched implements OrderLoaderEvent { throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$SummaryFetchedImplCopyWith<$Res> { + factory _$$SummaryFetchedImplCopyWith( + _$SummaryFetchedImpl value, + $Res Function(_$SummaryFetchedImpl) then, + ) = __$$SummaryFetchedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SummaryFetchedImplCopyWithImpl<$Res> + extends _$OrderLoaderEventCopyWithImpl<$Res, _$SummaryFetchedImpl> + implements _$$SummaryFetchedImplCopyWith<$Res> { + __$$SummaryFetchedImplCopyWithImpl( + _$SummaryFetchedImpl _value, + $Res Function(_$SummaryFetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OrderLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$SummaryFetchedImpl implements _SummaryFetched { + const _$SummaryFetchedImpl(); + + @override + String toString() { + return 'OrderLoaderEvent.summaryFetched()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$SummaryFetchedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, + required TResult Function(String status) statusChanged, + required TResult Function(String search) searchChanged, + required TResult Function(String? outletId) outletChanged, + required TResult Function(bool isRefresh) fetched, + required TResult Function() summaryFetched, + }) { + return summaryFetched(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult? Function(String status)? statusChanged, + TResult? Function(String search)? searchChanged, + TResult? Function(String? outletId)? outletChanged, + TResult? Function(bool isRefresh)? fetched, + TResult? Function()? summaryFetched, + }) { + return summaryFetched?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult Function(String status)? statusChanged, + TResult Function(String search)? searchChanged, + TResult Function(String? outletId)? outletChanged, + TResult Function(bool isRefresh)? fetched, + TResult Function()? summaryFetched, + required TResult orElse(), + }) { + if (summaryFetched != null) { + return summaryFetched(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, + required TResult Function(_StatusChanged value) statusChanged, + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_OutletChanged value) outletChanged, + required TResult Function(_Fetched value) fetched, + required TResult Function(_SummaryFetched value) summaryFetched, + }) { + return summaryFetched(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, + TResult? Function(_StatusChanged value)? statusChanged, + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_OutletChanged value)? outletChanged, + TResult? Function(_Fetched value)? fetched, + TResult? Function(_SummaryFetched value)? summaryFetched, + }) { + return summaryFetched?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, + TResult Function(_StatusChanged value)? statusChanged, + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_OutletChanged value)? outletChanged, + TResult Function(_Fetched value)? fetched, + TResult Function(_SummaryFetched value)? summaryFetched, + required TResult orElse(), + }) { + if (summaryFetched != null) { + return summaryFetched(this); + } + return orElse(); + } +} + +abstract class _SummaryFetched implements OrderLoaderEvent { + const factory _SummaryFetched() = _$SummaryFetchedImpl; +} + /// @nodoc mixin _$OrderLoaderState { List get orders => throw _privateConstructorUsedError; @@ -926,6 +1094,9 @@ mixin _$OrderLoaderState { bool get isFetching => throw _privateConstructorUsedError; bool get hasReachedMax => throw _privateConstructorUsedError; int get page => throw _privateConstructorUsedError; + int get totalOrder => throw _privateConstructorUsedError; + int get totalPendingOrder => throw _privateConstructorUsedError; + bool get isFetchingSummary => throw _privateConstructorUsedError; DateTime get dateFrom => throw _privateConstructorUsedError; DateTime get dateTo => throw _privateConstructorUsedError; @@ -952,6 +1123,9 @@ abstract class $OrderLoaderStateCopyWith<$Res> { bool isFetching, bool hasReachedMax, int page, + int totalOrder, + int totalPendingOrder, + bool isFetchingSummary, DateTime dateFrom, DateTime dateTo, }); @@ -980,6 +1154,9 @@ class _$OrderLoaderStateCopyWithImpl<$Res, $Val extends OrderLoaderState> Object? isFetching = null, Object? hasReachedMax = null, Object? page = null, + Object? totalOrder = null, + Object? totalPendingOrder = null, + Object? isFetchingSummary = null, Object? dateFrom = null, Object? dateTo = null, }) { @@ -1017,6 +1194,18 @@ class _$OrderLoaderStateCopyWithImpl<$Res, $Val extends OrderLoaderState> ? _value.page : page // ignore: cast_nullable_to_non_nullable as int, + totalOrder: null == totalOrder + ? _value.totalOrder + : totalOrder // ignore: cast_nullable_to_non_nullable + as int, + totalPendingOrder: null == totalPendingOrder + ? _value.totalPendingOrder + : totalPendingOrder // ignore: cast_nullable_to_non_nullable + as int, + isFetchingSummary: null == isFetchingSummary + ? _value.isFetchingSummary + : isFetchingSummary // ignore: cast_nullable_to_non_nullable + as bool, dateFrom: null == dateFrom ? _value.dateFrom : dateFrom // ignore: cast_nullable_to_non_nullable @@ -1049,6 +1238,9 @@ abstract class _$$OrderLoaderStateImplCopyWith<$Res> bool isFetching, bool hasReachedMax, int page, + int totalOrder, + int totalPendingOrder, + bool isFetchingSummary, DateTime dateFrom, DateTime dateTo, }); @@ -1076,6 +1268,9 @@ class __$$OrderLoaderStateImplCopyWithImpl<$Res> Object? isFetching = null, Object? hasReachedMax = null, Object? page = null, + Object? totalOrder = null, + Object? totalPendingOrder = null, + Object? isFetchingSummary = null, Object? dateFrom = null, Object? dateTo = null, }) { @@ -1113,6 +1308,18 @@ class __$$OrderLoaderStateImplCopyWithImpl<$Res> ? _value.page : page // ignore: cast_nullable_to_non_nullable as int, + totalOrder: null == totalOrder + ? _value.totalOrder + : totalOrder // ignore: cast_nullable_to_non_nullable + as int, + totalPendingOrder: null == totalPendingOrder + ? _value.totalPendingOrder + : totalPendingOrder // ignore: cast_nullable_to_non_nullable + as int, + isFetchingSummary: null == isFetchingSummary + ? _value.isFetchingSummary + : isFetchingSummary // ignore: cast_nullable_to_non_nullable + as bool, dateFrom: null == dateFrom ? _value.dateFrom : dateFrom // ignore: cast_nullable_to_non_nullable @@ -1138,6 +1345,9 @@ class _$OrderLoaderStateImpl implements _OrderLoaderState { this.isFetching = false, this.hasReachedMax = false, this.page = 1, + this.totalOrder = 0, + this.totalPendingOrder = 0, + this.isFetchingSummary = false, required this.dateFrom, required this.dateTo, }) : _orders = orders; @@ -1168,13 +1378,22 @@ class _$OrderLoaderStateImpl implements _OrderLoaderState { @JsonKey() final int page; @override + @JsonKey() + final int totalOrder; + @override + @JsonKey() + final int totalPendingOrder; + @override + @JsonKey() + final bool isFetchingSummary; + @override final DateTime dateFrom; @override final DateTime dateTo; @override String toString() { - return 'OrderLoaderState(orders: $orders, failureOptionOrder: $failureOptionOrder, status: $status, search: $search, outletId: $outletId, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page, dateFrom: $dateFrom, dateTo: $dateTo)'; + return 'OrderLoaderState(orders: $orders, failureOptionOrder: $failureOptionOrder, status: $status, search: $search, outletId: $outletId, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page, totalOrder: $totalOrder, totalPendingOrder: $totalPendingOrder, isFetchingSummary: $isFetchingSummary, dateFrom: $dateFrom, dateTo: $dateTo)'; } @override @@ -1194,6 +1413,12 @@ class _$OrderLoaderStateImpl implements _OrderLoaderState { (identical(other.hasReachedMax, hasReachedMax) || other.hasReachedMax == hasReachedMax) && (identical(other.page, page) || other.page == page) && + (identical(other.totalOrder, totalOrder) || + other.totalOrder == totalOrder) && + (identical(other.totalPendingOrder, totalPendingOrder) || + other.totalPendingOrder == totalPendingOrder) && + (identical(other.isFetchingSummary, isFetchingSummary) || + other.isFetchingSummary == isFetchingSummary) && (identical(other.dateFrom, dateFrom) || other.dateFrom == dateFrom) && (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); @@ -1210,6 +1435,9 @@ class _$OrderLoaderStateImpl implements _OrderLoaderState { isFetching, hasReachedMax, page, + totalOrder, + totalPendingOrder, + isFetchingSummary, dateFrom, dateTo, ); @@ -1236,6 +1464,9 @@ abstract class _OrderLoaderState implements OrderLoaderState { final bool isFetching, final bool hasReachedMax, final int page, + final int totalOrder, + final int totalPendingOrder, + final bool isFetchingSummary, required final DateTime dateFrom, required final DateTime dateTo, }) = _$OrderLoaderStateImpl; @@ -1257,6 +1488,12 @@ abstract class _OrderLoaderState implements OrderLoaderState { @override int get page; @override + int get totalOrder; + @override + int get totalPendingOrder; + @override + bool get isFetchingSummary; + @override DateTime get dateFrom; @override DateTime get dateTo; diff --git a/lib/application/order/order_loader/order_loader_event.dart b/lib/application/order/order_loader/order_loader_event.dart index 615c66f..7c49673 100644 --- a/lib/application/order/order_loader/order_loader_event.dart +++ b/lib/application/order/order_loader/order_loader_event.dart @@ -12,4 +12,5 @@ class OrderLoaderEvent with _$OrderLoaderEvent { _OutletChanged; const factory OrderLoaderEvent.fetched({@Default(false) bool isRefresh}) = _Fetched; + const factory OrderLoaderEvent.summaryFetched() = _SummaryFetched; } diff --git a/lib/application/order/order_loader/order_loader_state.dart b/lib/application/order/order_loader/order_loader_state.dart index 894f60b..ab39442 100644 --- a/lib/application/order/order_loader/order_loader_state.dart +++ b/lib/application/order/order_loader/order_loader_state.dart @@ -11,6 +11,9 @@ class OrderLoaderState with _$OrderLoaderState { @Default(false) bool isFetching, @Default(false) bool hasReachedMax, @Default(1) int page, + @Default(0) int totalOrder, + @Default(0) int totalPendingOrder, + @Default(false) bool isFetchingSummary, required DateTime dateFrom, required DateTime dateTo, }) = _OrderLoaderState; diff --git a/lib/domain/order/repositories/i_order_repository.dart b/lib/domain/order/repositories/i_order_repository.dart index 7f13301..011fe03 100644 --- a/lib/domain/order/repositories/i_order_repository.dart +++ b/lib/domain/order/repositories/i_order_repository.dart @@ -10,4 +10,11 @@ abstract class IOrderRepository { required DateTime dateFrom, required DateTime dateTo, }); + + Future> getTotalCount({ + String? status, + String? outletId, + required DateTime dateFrom, + required DateTime dateTo, + }); } diff --git a/lib/infrastructure/order/datasource/remote_data_provider.dart b/lib/infrastructure/order/datasource/remote_data_provider.dart index ecc4fda..80ab40e 100644 --- a/lib/infrastructure/order/datasource/remote_data_provider.dart +++ b/lib/infrastructure/order/datasource/remote_data_provider.dart @@ -43,9 +43,9 @@ class OrderRemoteDataProvider { params['search'] = search; } - // if (outletId != null) { - // params['outlet_id'] = outletId; - // } + if (outletId != null) { + params['outlet_id'] = outletId; + } final response = await _apiClient.get( ApiPath.order, @@ -67,4 +67,47 @@ class OrderRemoteDataProvider { return DC.error(OrderFailure.serverError(e)); } } + + /// Backend belum punya endpoint ringkasan order, jadi jumlahnya diambil + /// dari `total_count` list order (limit 1 supaya payload kecil). + Future> fetchTotalCount({ + String? status, + String? outletId, + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + Map params = { + 'page': 1, + 'limit': 1, + 'date_from': dateFrom.toServerDate, + 'date_to': dateTo.toServerDate, + }; + + if (status != null && status.isNotEmpty) { + params['status'] = status; + } + + if (outletId != null) { + params['outlet_id'] = outletId; + } + + final response = await _apiClient.get( + ApiPath.order, + params: params, + headers: getAuthorizationHeader(), + ); + + if (response.data['data'] == null) { + return DC.error(OrderFailure.empty()); + } + + final total = response.data['data']['total_count'] as num? ?? 0; + + return DC.data(total.toInt()); + } on ApiFailure catch (e, s) { + log('fetchOrderTotalCountError', name: _logName, error: e, stackTrace: s); + return DC.error(OrderFailure.serverError(e)); + } + } } diff --git a/lib/infrastructure/order/repositories/order_repository.dart b/lib/infrastructure/order/repositories/order_repository.dart index 6317fe8..6580a68 100644 --- a/lib/infrastructure/order/repositories/order_repository.dart +++ b/lib/infrastructure/order/repositories/order_repository.dart @@ -4,16 +4,15 @@ import 'package:dartz/dartz.dart' hide Order; import 'package:injectable/injectable.dart' hide Order; import '../../../domain/order/order.dart'; -import '../../outlet/datasource/local_data_provider.dart'; import '../datasource/remote_data_provider.dart'; +/// `outletId` null = semua outlet (sesuai pilihan "Semua Outlet" di filter). @Injectable(as: IOrderRepository) class OrderRepository implements IOrderRepository { final OrderRemoteDataProvider _dataProvider; - final OutletLocalDataProvider _outletLocalDataProvider; final String _logName = 'OrderRepository'; - OrderRepository(this._dataProvider, this._outletLocalDataProvider); + OrderRepository(this._dataProvider); @override Future>> get({ @@ -26,15 +25,12 @@ class OrderRepository implements IOrderRepository { required DateTime dateTo, }) async { try { - final resolvedOutletId = - outletId ?? _outletLocalDataProvider.getSelectedOutletId(); - final result = await _dataProvider.fetch( page: page, limit: limit, status: status, search: search, - outletId: resolvedOutletId, + outletId: outletId, dateFrom: dateFrom, dateTo: dateTo, ); @@ -51,4 +47,30 @@ class OrderRepository implements IOrderRepository { return left(const OrderFailure.unexpectedError()); } } + + @override + Future> getTotalCount({ + String? status, + String? outletId, + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + final result = await _dataProvider.fetchTotalCount( + status: status, + outletId: outletId, + dateFrom: dateFrom, + dateTo: dateTo, + ); + + if (result.hasError) { + return left(result.error!); + } + + return right(result.data!); + } catch (e, s) { + log('getOrderTotalCountError', name: _logName, error: e, stackTrace: s); + return left(const OrderFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 1c0881c..891deb3 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -207,6 +207,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i48.ICustomerRepository>( () => _i550.CustomerRepository(gh<_i1006.CustomerRemoteDataProvider>()), ); + gh.factory<_i219.IOrderRepository>( + () => _i641.OrderRepository(gh<_i130.OrderRemoteDataProvider>()), + ); gh.factory<_i49.IAuthRepository>( () => _i1035.AuthRepository( gh<_i991.AuthLocalDataProvider>(), @@ -225,12 +228,6 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i419.IProductRepository>( () => _i121.ProductRepository(gh<_i823.ProductRemoteDataProvider>()), ); - gh.factory<_i219.IOrderRepository>( - () => _i641.OrderRepository( - gh<_i130.OrderRemoteDataProvider>(), - gh<_i850.OutletLocalDataProvider>(), - ), - ); gh.factory<_i972.CustomerLoaderBloc>( () => _i972.CustomerLoaderBloc(gh<_i48.ICustomerRepository>()), ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ac5fb86..c0e01a1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -621,5 +621,14 @@ "status_critical": "Unhealthy", "@status_critical": {}, "tap_row_for_detail": "Tap a card to see sub category and product details", - "@tap_row_for_detail": {} + "@tap_row_for_detail": {}, + "order_unpaid_ratio": "{percent}% of orders still awaiting payment", + "@order_unpaid_ratio": { + "placeholders": { + "percent": { + "type": "String", + "example": "25" + } + } + } } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 2059651..c799fbc 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -621,5 +621,14 @@ "status_critical": "Tidak Sehat", "@status_critical": {}, "tap_row_for_detail": "Ketuk kartu untuk melihat rincian sub kategori dan produk", - "@tap_row_for_detail": {} + "@tap_row_for_detail": {}, + "order_unpaid_ratio": "{percent}% pesanan masih menunggu pembayaran", + "@order_unpaid_ratio": { + "placeholders": { + "percent": { + "type": "String", + "example": "25" + } + } + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e57b8c2..6b9d223 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1750,6 +1750,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Tap a card to see sub category and product details'** String get tap_row_for_detail; + + /// No description provided for @order_unpaid_ratio. + /// + /// In en, this message translates to: + /// **'{percent}% of orders still awaiting payment'** + String order_unpaid_ratio(String percent); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 477055e..15587f6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -855,4 +855,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tap_row_for_detail => 'Tap a card to see sub category and product details'; + + @override + String order_unpaid_ratio(String percent) { + return '$percent% of orders still awaiting payment'; + } } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 393a0bc..9ef0c4a 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -855,4 +855,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get tap_row_for_detail => 'Ketuk kartu untuk melihat rincian sub kategori dan produk'; + + @override + String order_unpaid_ratio(String percent) { + return '$percent% pesanan masih menunggu pembayaran'; + } } diff --git a/lib/presentation/pages/order/order_list/order_page.dart b/lib/presentation/pages/order/order_list/order_page.dart index 85bbe97..6455d1d 100644 --- a/lib/presentation/pages/order/order_list/order_page.dart +++ b/lib/presentation/pages/order/order_list/order_page.dart @@ -14,6 +14,7 @@ import '../../../components/appbar/appbar.dart'; import '../../../components/widgets/empty_widget.dart'; import '../../../router/app_router.gr.dart'; import 'widgets/filter_header_delegate.dart'; +import 'widgets/order_summary.dart'; import 'widgets/order_tile.dart'; @RoutePage() @@ -29,7 +30,8 @@ class OrderPage extends StatefulWidget implements AutoRouteWrapper { BlocProvider( create: (_) => getIt() - ..add(OrderLoaderEvent.fetched(isRefresh: true)), + ..add(OrderLoaderEvent.fetched(isRefresh: true)) + ..add(const OrderLoaderEvent.summaryFetched()), ), BlocProvider( create: (_) => @@ -192,9 +194,9 @@ class _OrderPageState extends State with TickerProviderStateMixin { BlocListener( listenWhen: (p, c) => p.outletId != c.outletId, listener: (context, state) { - context.read().add( - OrderLoaderEvent.fetched(isRefresh: true), - ); + context.read() + ..add(OrderLoaderEvent.fetched(isRefresh: true)) + ..add(const OrderLoaderEvent.summaryFetched()); }, ), BlocListener( @@ -210,9 +212,9 @@ class _OrderPageState extends State with TickerProviderStateMixin { previous.dateFrom != current.dateFrom || previous.dateTo != current.dateTo, listener: (context, state) { - context.read().add( - OrderLoaderEvent.fetched(isRefresh: true), - ); + context.read() + ..add(OrderLoaderEvent.fetched(isRefresh: true)) + ..add(const OrderLoaderEvent.summaryFetched()); }, ), ], @@ -301,6 +303,14 @@ class _OrderPageState extends State with TickerProviderStateMixin { position: _slideAnimation, child: Column( children: [ + OrderSummary( + totalOrder: state.totalOrder, + totalPendingOrder: + state.totalPendingOrder, + isLoading: state.isFetchingSummary, + ), + const SizedBox(height: 16), + // Show filtered transaction count if (state.status != 'all' && !state.isFetching) diff --git a/lib/presentation/pages/order/order_list/widgets/order_summary.dart b/lib/presentation/pages/order/order_list/widgets/order_summary.dart new file mode 100644 index 0000000..e9451d6 --- /dev/null +++ b/lib/presentation/pages/order/order_list/widgets/order_summary.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../../../../../common/extension/extension.dart'; +import '../../../../../common/theme/theme.dart'; +import '../../../../components/spacer/spacer.dart'; +import '../../../../components/widgets/particle_card.dart'; + +/// Aksen pesanan pending — amber supaya kontras di atas gradient merah. +const Color _pendingColor = Color(0xFFFFB300); + +class OrderSummary extends StatelessWidget { + final int totalOrder; + final int totalPendingOrder; + final bool isLoading; + + const OrderSummary({ + super.key, + required this.totalOrder, + required this.totalPendingOrder, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + final pendingRatio = totalOrder > 0 + ? (totalPendingOrder / totalOrder).clamp(0.0, 1.0) + : 0.0; + + return ParticleCard( + borderRadius: 18, + decorationOpacity: 0.8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: _SummaryStat( + title: context.lang.total_orders, + value: totalOrder, + icon: Icons.receipt_long_rounded, + iconBackground: AppColor.white.withOpacity(0.2), + isLoading: isLoading, + ), + ), + VerticalDivider( + width: 28, + thickness: 1, + color: AppColor.white.withOpacity(0.25), + ), + Expanded( + child: _SummaryStat( + title: context.lang.pending_order, + value: totalPendingOrder, + icon: Icons.hourglass_top_rounded, + iconBackground: _pendingColor, + isLoading: isLoading, + ), + ), + ], + ), + ), + const SpaceHeight(16), + _PendingRatioBar(ratio: pendingRatio), + const SpaceHeight(8), + Text( + context.lang.order_unpaid_ratio( + (pendingRatio * 100).round().toString(), + ), + style: AppStyle.xs.copyWith( + color: AppColor.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} + +class _SummaryStat extends StatelessWidget { + final String title; + final int value; + final IconData icon; + final Color iconBackground; + final bool isLoading; + + const _SummaryStat({ + required this.title, + required this.value, + required this.icon, + required this.iconBackground, + required this.isLoading, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: iconBackground, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: AppColor.white, size: 16), + ), + const SpaceWidth(8), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppStyle.sm.copyWith( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SpaceHeight(12), + if (isLoading) + Shimmer.fromColors( + baseColor: AppColor.white.withOpacity(0.2), + highlightColor: AppColor.white.withOpacity(0.45), + child: Container( + width: 56, + height: 30, + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(6), + ), + ), + ) + else + TweenAnimationBuilder( + tween: IntTween(begin: 0, end: value), + duration: const Duration(milliseconds: 900), + curve: Curves.easeOutCubic, + builder: (context, animatedValue, _) => Text( + animatedValue.thousandFormat, + style: AppStyle.h1.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w900, + fontSize: 28, + height: 1.1, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _PendingRatioBar extends StatelessWidget { + final double ratio; + + const _PendingRatioBar({required this.ratio}); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Container( + height: 6, + color: AppColor.white.withOpacity(0.2), + alignment: Alignment.centerLeft, + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: ratio), + duration: const Duration(milliseconds: 900), + curve: Curves.easeOutCubic, + builder: (context, value, _) => FractionallySizedBox( + widthFactor: value, + heightFactor: 1, + child: Container( + decoration: BoxDecoration( + color: _pendingColor, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 81265d9..868a9e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Flutter project." publish_to: "none" -version: 1.0.4+14 +version: 1.0.4+15 environment: sdk: ^3.8.1