Compare commits

..
22 Commits
Author SHA1 Message Date
Efril 6ebfae9d5b Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
2026-05-29 17:37:52 +07:00
Efril 926e45170d update workflow 2026-05-29 17:37:25 +07:00
efrilm f9cbd16b28 update 2026-05-29 16:09:48 +07:00
Efril 3ae2e93531 update workflow 2026-05-29 16:00:38 +07:00
Efril a182a3504c Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter 2026-05-29 15:51:43 +07:00
Efril 88972b28b5 update pod 2026-05-29 15:51:02 +07:00
Efril 2c8e9a3fb4 update 2026-05-29 15:12:05 +07:00
efrilm 086c17a217 add crashlytic 2026-05-25 19:54:04 +07:00
efrilm 9a7681680d update apns 2026-05-25 19:32:38 +07:00
Efril 823e009121 purchasing 2026-05-19 23:33:12 +07:00
Efril 602647ff26 Merge branch 'main' of https://gits.altru.id/apksel-dev/apskel-owner-flutter 2026-05-19 23:08:20 +07:00
Efril d9a553708b purchasing analytic repo 2026-05-19 23:08:01 +07:00
efrilm aaa8eba31b update workflow 2026-05-15 13:44:06 +07:00
efrilm 31a82e14cb update workflows 2026-05-15 13:36:13 +07:00
Efril e88eff28f0 update 2026-05-15 13:26:53 +07:00
Efril 720c63b6c6 Merge branch 'main' of https://github.com/efrilm/apskel-owner 2026-05-15 13:21:09 +07:00
Efril ca2cc8bf6b update ios 2026-05-15 13:19:20 +07:00
efrilm d56c3f89db update workflow 2026-05-15 13:08:05 +07:00
efrilm f7f5682924 update workflow 2026-05-15 13:02:41 +07:00
efrilm b51749a01b add github actions ios build 2026-05-15 12:58:24 +07:00
aefril 5fd870c507 Merge pull request 'git push' (#2) from dev into main
Reviewed-on: #2
2026-05-15 05:53:32 +00:00
aefril d8d8fd9d16 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-05-15 05:52:25 +00:00
37 changed files with 6078 additions and 568 deletions
+110
View File
@@ -0,0 +1,110 @@
name: Build & Deploy iOS to TestFlight
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: macos-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.29.3"
channel: "stable"
cache: true
- name: Install Dependencies
run: flutter pub get
# ── Code Signing Setup ──────────────────────────────────────────────────
- name: Install Apple Certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# Decode certificate to file
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A \
-t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Install Provisioning Profile
env:
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
run: |
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# ── CocoaPods ───────────────────────────────────────────────────────────
- name: Install CocoaPods Dependencies
run: |
cd ios
rm -f Podfile.lock
pod install --repo-update
# ── Build ───────────────────────────────────────────────────────────────
- name: Build iOS IPA
env:
BUNDLE_ID: ${{ secrets.BUNDLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
run: |
flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
# ── Upload to TestFlight ────────────────────────────────────────────────
- name: Upload to TestFlight via Transporter
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
xcrun altool --upload-app \
--type ios \
--file "build/ios/ipa/*.ipa" \
--username "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--verbose
# ── Cleanup ─────────────────────────────────────────────────────────────
- name: Clean Up Keychain and Provisioning Profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision || true
# ── Artifact (opsional, untuk debugging) ────────────────────────────────
- name: Upload IPA as Artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: build/ios/ipa/*.ipa
retention-days: 7
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Ganti "app-store-connect" jika perlu: ad-hoc, enterprise, development -->
<key>method</key>
<string>app-store-connect</string>
<!-- Team ID dari Apple Developer account -->
<key>teamID</key>
<string>5TRC3M8UZG</string>
<!-- Signing style: manual (pakai provisioning profile eksplisit) -->
<key>signingStyle</key>
<string>manual</string>
<!-- Provisioning profiles: key = bundle ID, value = nama profile -->
<key>provisioningProfiles</key>
<dict>
<key>com.apskel.enaklo</key>
<string>Enaklo Owner App Store</string>
</dict>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
</dict>
</plist>
+4
View File
@@ -39,5 +39,9 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
end
end end
end end
+1 -1
View File
@@ -176,6 +176,6 @@ SPEC CHECKSUMS:
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: e30f02f9d1c72c47bb6344a0a748c9d268180865 PODFILE CHECKSUM: ecb39f0da990336cc02cd0b433a02558e58ace35
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+1
View File
@@ -0,0 +1 @@
2edd7a931624573eff7b4280d22a7f4e
@@ -0,0 +1,55 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../domain/analytic/repositories/i_analytic_repository.dart';
part 'purchasing_analytic_loader_event.dart';
part 'purchasing_analytic_loader_state.dart';
part 'purchasing_analytic_loader_bloc.freezed.dart';
@injectable
class PurchasingAnalyticLoaderBloc
extends Bloc<PurchasingAnalyticLoaderEvent, PurchasingAnalyticLoaderState> {
final IAnalyticRepository _analyticRepository;
PurchasingAnalyticLoaderBloc(this._analyticRepository)
: super(PurchasingAnalyticLoaderState.initial()) {
on<PurchasingAnalyticLoaderEvent>(_onEvent);
}
Future<void> _onEvent(
PurchasingAnalyticLoaderEvent event,
Emitter<PurchasingAnalyticLoaderState> emit,
) {
return event.map(
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(),
));
final result = await _analyticRepository.getPurchasing(
dateFrom: state.dateFrom,
dateTo: state.dateTo,
outletId: state.outletId,
);
final newState = result.fold(
(f) => state.copyWith(failureOptionPurchasing: optionOf(f)),
(purchasing) => state.copyWith(purchasing: purchasing),
);
emit(newState.copyWith(isFetching: false));
},
);
}
}
@@ -0,0 +1,818 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'purchasing_analytic_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$PurchasingAnalyticLoaderEvent {
@optionalTypeArgs
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;
}
/// @nodoc
abstract class $PurchasingAnalyticLoaderEventCopyWith<$Res> {
factory $PurchasingAnalyticLoaderEventCopyWith(
PurchasingAnalyticLoaderEvent value,
$Res Function(PurchasingAnalyticLoaderEvent) then,
) =
_$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
PurchasingAnalyticLoaderEvent
>;
}
/// @nodoc
class _$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
$Val extends PurchasingAnalyticLoaderEvent
>
implements $PurchasingAnalyticLoaderEventCopyWith<$Res> {
_$PurchasingAnalyticLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$RangeDateChangedImplCopyWith<$Res> {
factory _$$RangeDateChangedImplCopyWith(
_$RangeDateChangedImpl value,
$Res Function(_$RangeDateChangedImpl) then,
) = __$$RangeDateChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({DateTime dateFrom, DateTime dateTo});
}
/// @nodoc
class __$$RangeDateChangedImplCopyWithImpl<$Res>
extends
_$PurchasingAnalyticLoaderEventCopyWithImpl<
$Res,
_$RangeDateChangedImpl
>
implements _$$RangeDateChangedImplCopyWith<$Res> {
__$$RangeDateChangedImplCopyWithImpl(
_$RangeDateChangedImpl _value,
$Res Function(_$RangeDateChangedImpl) _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? dateFrom = null, Object? dateTo = null}) {
return _then(
_$RangeDateChangedImpl(
null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$RangeDateChangedImpl implements _RangeDateChanged {
const _$RangeDateChangedImpl(this.dateFrom, this.dateTo);
@override
final DateTime dateFrom;
@override
final DateTime dateTo;
@override
String toString() {
return 'PurchasingAnalyticLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RangeDateChangedImpl &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
}
@override
int get hashCode => Object.hash(runtimeType, dateFrom, dateTo);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
__$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime dateFrom, DateTime dateTo)
rangeDateChanged,
required TResult Function(String? outletId) outletChanged,
required TResult Function() fetched,
}) {
return rangeDateChanged(dateFrom, dateTo);
}
@override
@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);
}
@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 (rangeDateChanged != null) {
return rangeDateChanged(dateFrom, dateTo);
}
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 rangeDateChanged(this);
}
@override
@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);
}
@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 (rangeDateChanged != null) {
return rangeDateChanged(this);
}
return orElse();
}
}
abstract class _RangeDateChanged implements PurchasingAnalyticLoaderEvent {
const factory _RangeDateChanged(
final DateTime dateFrom,
final DateTime dateTo,
) = _$RangeDateChangedImpl;
DateTime get dateFrom;
DateTime get dateTo;
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$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(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$PurchasingAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl();
@override
String toString() {
return 'PurchasingAnalyticLoaderEvent.fetched()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$FetchedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@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 fetched();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged,
TResult? Function(String? outletId)? outletChanged,
TResult? Function()? fetched,
}) {
return fetched?.call();
}
@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 (fetched != null) {
return fetched();
}
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 fetched(this);
}
@override
@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);
}
@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 (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements PurchasingAnalyticLoaderEvent {
const factory _Fetched() = _$FetchedImpl;
}
/// @nodoc
mixin _$PurchasingAnalyticLoaderState {
PurchasingAnalytic get purchasing => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOptionPurchasing =>
throw _privateConstructorUsedError;
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.
@JsonKey(includeFromJson: false, includeToJson: false)
$PurchasingAnalyticLoaderStateCopyWith<PurchasingAnalyticLoaderState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PurchasingAnalyticLoaderStateCopyWith<$Res> {
factory $PurchasingAnalyticLoaderStateCopyWith(
PurchasingAnalyticLoaderState value,
$Res Function(PurchasingAnalyticLoaderState) then,
) =
_$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
PurchasingAnalyticLoaderState
>;
@useResult
$Res call({
PurchasingAnalytic purchasing,
Option<AnalyticFailure> failureOptionPurchasing,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
String? outletId,
});
$PurchasingAnalyticCopyWith<$Res> get purchasing;
}
/// @nodoc
class _$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
$Val extends PurchasingAnalyticLoaderState
>
implements $PurchasingAnalyticLoaderStateCopyWith<$Res> {
_$PurchasingAnalyticLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchasing = null,
Object? failureOptionPurchasing = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
Object? outletId = freezed,
}) {
return _then(
_value.copyWith(
purchasing: null == purchasing
? _value.purchasing
: purchasing // ignore: cast_nullable_to_non_nullable
as PurchasingAnalytic,
failureOptionPurchasing: null == failureOptionPurchasing
? _value.failureOptionPurchasing
: failureOptionPurchasing // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
outletId: freezed == outletId
? _value.outletId
: outletId // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PurchasingAnalyticCopyWith<$Res> get purchasing {
return $PurchasingAnalyticCopyWith<$Res>(_value.purchasing, (value) {
return _then(_value.copyWith(purchasing: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PurchasingAnalyticLoaderStateImplCopyWith<$Res>
implements $PurchasingAnalyticLoaderStateCopyWith<$Res> {
factory _$$PurchasingAnalyticLoaderStateImplCopyWith(
_$PurchasingAnalyticLoaderStateImpl value,
$Res Function(_$PurchasingAnalyticLoaderStateImpl) then,
) = __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
PurchasingAnalytic purchasing,
Option<AnalyticFailure> failureOptionPurchasing,
bool isFetching,
DateTime dateFrom,
DateTime dateTo,
String? outletId,
});
@override
$PurchasingAnalyticCopyWith<$Res> get purchasing;
}
/// @nodoc
class __$$PurchasingAnalyticLoaderStateImplCopyWithImpl<$Res>
extends
_$PurchasingAnalyticLoaderStateCopyWithImpl<
$Res,
_$PurchasingAnalyticLoaderStateImpl
>
implements _$$PurchasingAnalyticLoaderStateImplCopyWith<$Res> {
__$$PurchasingAnalyticLoaderStateImplCopyWithImpl(
_$PurchasingAnalyticLoaderStateImpl _value,
$Res Function(_$PurchasingAnalyticLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? purchasing = null,
Object? failureOptionPurchasing = null,
Object? isFetching = null,
Object? dateFrom = null,
Object? dateTo = null,
Object? outletId = freezed,
}) {
return _then(
_$PurchasingAnalyticLoaderStateImpl(
purchasing: null == purchasing
? _value.purchasing
: purchasing // ignore: cast_nullable_to_non_nullable
as PurchasingAnalytic,
failureOptionPurchasing: null == failureOptionPurchasing
? _value.failureOptionPurchasing
: failureOptionPurchasing // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
dateFrom: null == dateFrom
? _value.dateFrom
: dateFrom // ignore: cast_nullable_to_non_nullable
as DateTime,
dateTo: null == dateTo
? _value.dateTo
: dateTo // ignore: cast_nullable_to_non_nullable
as DateTime,
outletId: freezed == outletId
? _value.outletId
: outletId // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$PurchasingAnalyticLoaderStateImpl
implements _PurchasingAnalyticLoaderState {
const _$PurchasingAnalyticLoaderStateImpl({
required this.purchasing,
required this.failureOptionPurchasing,
this.isFetching = false,
required this.dateFrom,
required this.dateTo,
this.outletId,
});
@override
final PurchasingAnalytic purchasing;
@override
final Option<AnalyticFailure> failureOptionPurchasing;
@override
@JsonKey()
final bool isFetching;
@override
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, outletId: $outletId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PurchasingAnalyticLoaderStateImpl &&
(identical(other.purchasing, purchasing) ||
other.purchasing == purchasing) &&
(identical(
other.failureOptionPurchasing,
failureOptionPurchasing,
) ||
other.failureOptionPurchasing == failureOptionPurchasing) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo) &&
(identical(other.outletId, outletId) ||
other.outletId == outletId));
}
@override
int get hashCode => Object.hash(
runtimeType,
purchasing,
failureOptionPurchasing,
isFetching,
dateFrom,
dateTo,
outletId,
);
/// Create a copy of PurchasingAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PurchasingAnalyticLoaderStateImplCopyWith<
_$PurchasingAnalyticLoaderStateImpl
>
get copyWith =>
__$$PurchasingAnalyticLoaderStateImplCopyWithImpl<
_$PurchasingAnalyticLoaderStateImpl
>(this, _$identity);
}
abstract class _PurchasingAnalyticLoaderState
implements PurchasingAnalyticLoaderState {
const factory _PurchasingAnalyticLoaderState({
required final PurchasingAnalytic purchasing,
required final Option<AnalyticFailure> failureOptionPurchasing,
final bool isFetching,
required final DateTime dateFrom,
required final DateTime dateTo,
final String? outletId,
}) = _$PurchasingAnalyticLoaderStateImpl;
@override
PurchasingAnalytic get purchasing;
@override
Option<AnalyticFailure> get failureOptionPurchasing;
@override
bool get isFetching;
@override
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.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PurchasingAnalyticLoaderStateImplCopyWith<
_$PurchasingAnalyticLoaderStateImpl
>
get copyWith => throw _privateConstructorUsedError;
}
@@ -0,0 +1,15 @@
part of 'purchasing_analytic_loader_bloc.dart';
@freezed
class PurchasingAnalyticLoaderEvent with _$PurchasingAnalyticLoaderEvent {
const factory PurchasingAnalyticLoaderEvent.rangeDateChanged(
DateTime dateFrom,
DateTime dateTo,
) = _RangeDateChanged;
const factory PurchasingAnalyticLoaderEvent.outletChanged(
String? outletId,
) = _OutletChanged;
const factory PurchasingAnalyticLoaderEvent.fetched() = _Fetched;
}
@@ -0,0 +1,21 @@
part of 'purchasing_analytic_loader_bloc.dart';
@freezed
class PurchasingAnalyticLoaderState with _$PurchasingAnalyticLoaderState {
const factory PurchasingAnalyticLoaderState({
required PurchasingAnalytic purchasing,
required Option<AnalyticFailure> failureOptionPurchasing,
@Default(false) bool isFetching,
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) = _PurchasingAnalyticLoaderState;
factory PurchasingAnalyticLoaderState.initial() =>
PurchasingAnalyticLoaderState(
purchasing: PurchasingAnalytic.empty(),
failureOptionPurchasing: none(),
dateFrom: DateTime.now().subtract(const Duration(days: 30)),
dateTo: DateTime.now(),
);
}
+3
View File
@@ -65,5 +65,8 @@ class ThemeApp {
), ),
iconTheme: const IconThemeData(color: AppColor.white), iconTheme: const IconThemeData(color: AppColor.white),
), ),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColor.white,
)
); );
} }
+1
View File
@@ -11,6 +11,7 @@ class ApiPath {
static const String productAnalytic = '/api/v1/analytics/products'; static const String productAnalytic = '/api/v1/analytics/products';
static const String paymentMethodAnalytic = static const String paymentMethodAnalytic =
'/api/v1/analytics/paymentMethods'; '/api/v1/analytics/paymentMethods';
static const String purchasingAnalytic = '/api/v1/analytics/purchasing';
// Inventory // Inventory
static const String inventoryReportDetail = static const String inventoryReportDetail =
+17 -8
View File
@@ -101,17 +101,26 @@ class FcmService {
} }
// 7. Print FCM token for debugging // 7. Print FCM token for debugging
final token = await getToken(); try {
debugPrint('[FCM] Token: $token'); final token = await getToken();
debugPrint('[FCM] Token: $token');
} catch (e) {
// Simulator atau APNs belum dikonfigurasi — skip token fetch
debugPrint('[FCM] Token unavailable: $e');
}
} }
Future<void> _requestPermission() async { Future<void> _requestPermission() async {
final settings = await _messaging.requestPermission( try {
alert: true, final settings = await _messaging.requestPermission(
badge: true, alert: true,
sound: true, badge: true,
); sound: true,
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}'); );
debugPrint('[FCM] Permission status: ${settings.authorizationStatus}');
} catch (e) {
debugPrint('[FCM] Permission request failed: $e');
}
} }
Future<void> _setupLocalNotifications() async { Future<void> _setupLocalNotifications() async {
+1
View File
@@ -11,4 +11,5 @@ part 'entities/inventory_analytic_entity.dart';
part 'entities/dashboard_analytic_entity.dart'; part 'entities/dashboard_analytic_entity.dart';
part 'entities/product_analytic_entity.dart'; part 'entities/product_analytic_entity.dart';
part 'entities/payment_method_analytic_entity.dart'; part 'entities/payment_method_analytic_entity.dart';
part 'entities/purchasing_analytic_entity.dart';
part 'failures/analytic_failure.dart'; part 'failures/analytic_failure.dart';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
part of '../analytic.dart';
@freezed
class PurchasingAnalytic with _$PurchasingAnalytic {
const factory PurchasingAnalytic({
required String organizationId,
required String outletId,
required String outletName,
required DateTime dateFrom,
required DateTime dateTo,
required String groupBy,
required PurchasingAnalyticSummary summary,
required List<PurchasingAnalyticData> data,
required List<PurchasingIngredientData> ingredientData,
required List<PurchasingVendorData> vendorData,
}) = _PurchasingAnalytic;
factory PurchasingAnalytic.empty() => PurchasingAnalytic(
organizationId: '',
outletId: '',
outletName: '',
dateFrom: DateTime.fromMillisecondsSinceEpoch(0),
dateTo: DateTime.fromMillisecondsSinceEpoch(0),
groupBy: '',
summary: PurchasingAnalyticSummary.empty(),
data: [],
ingredientData: [],
vendorData: [],
);
}
@freezed
class PurchasingAnalyticSummary with _$PurchasingAnalyticSummary {
const factory PurchasingAnalyticSummary({
required int totalPurchases,
required int totalPurchaseOrders,
required int totalQuantity,
required double averagePurchaseOrderValue,
required int totalIngredients,
required int totalVendors,
}) = _PurchasingAnalyticSummary;
factory PurchasingAnalyticSummary.empty() =>
const PurchasingAnalyticSummary(
totalPurchases: 0,
totalPurchaseOrders: 0,
totalQuantity: 0,
averagePurchaseOrderValue: 0,
totalIngredients: 0,
totalVendors: 0,
);
}
@freezed
class PurchasingAnalyticData with _$PurchasingAnalyticData {
const factory PurchasingAnalyticData({
required DateTime date,
required int purchases,
required int purchaseOrders,
required int quantity,
required int ingredients,
required int vendors,
}) = _PurchasingAnalyticData;
factory PurchasingAnalyticData.empty() => PurchasingAnalyticData(
date: DateTime.fromMillisecondsSinceEpoch(0),
purchases: 0,
purchaseOrders: 0,
quantity: 0,
ingredients: 0,
vendors: 0,
);
}
@freezed
class PurchasingIngredientData with _$PurchasingIngredientData {
const factory PurchasingIngredientData({
required String ingredientId,
required String ingredientName,
required int quantity,
required int totalCost,
required double averageUnitCost,
required int purchaseOrderCount,
}) = _PurchasingIngredientData;
factory PurchasingIngredientData.empty() =>
const PurchasingIngredientData(
ingredientId: '',
ingredientName: '',
quantity: 0,
totalCost: 0,
averageUnitCost: 0,
purchaseOrderCount: 0,
);
}
@freezed
class PurchasingVendorData with _$PurchasingVendorData {
const factory PurchasingVendorData({
required String vendorId,
required String vendorName,
required int totalCost,
required int purchaseOrderCount,
required int ingredientCount,
required int quantity,
}) = _PurchasingVendorData;
factory PurchasingVendorData.empty() => const PurchasingVendorData(
vendorId: '',
vendorName: '',
totalCost: 0,
purchaseOrderCount: 0,
ingredientCount: 0,
quantity: 0,
);
}
@@ -44,4 +44,10 @@ abstract class IAnalyticRepository {
required DateTime dateTo, required DateTime dateTo,
String? outletId, String? outletId,
}); });
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
});
} }
@@ -12,3 +12,4 @@ part 'dto/inventory_analytic_dto.dart';
part 'dto/dashboard_analytic_dto.dart'; part 'dto/dashboard_analytic_dto.dart';
part 'dto/product_analytic_dto.dart'; part 'dto/product_analytic_dto.dart';
part 'dto/payment_method_analytic_dto.dart'; part 'dto/payment_method_analytic_dto.dart';
part 'dto/purchasing_analytic_dto.dart';
File diff suppressed because it is too large Load Diff
@@ -637,3 +637,139 @@ Map<String, dynamic> _$$PaymentMethodSummaryDtoImplToJson(
'total_payments': instance.totalPayments, 'total_payments': instance.totalPayments,
'average_order_value': instance.averageOrderValue, 'average_order_value': instance.averageOrderValue,
}; };
_$PurchasingAnalyticDtoImpl _$$PurchasingAnalyticDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticDtoImpl(
organizationId: json['organization_id'] as String?,
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
dateFrom: json['date_from'] == null
? null
: DateTime.parse(json['date_from'] as String),
dateTo: json['date_to'] == null
? null
: DateTime.parse(json['date_to'] as String),
groupBy: json['group_by'] as String?,
summary: json['summary'] == null
? null
: PurchasingAnalyticSummaryDto.fromJson(
json['summary'] as Map<String, dynamic>,
),
data: (json['data'] as List<dynamic>?)
?.map(
(e) => PurchasingAnalyticDataDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
ingredientData: (json['ingredient_data'] as List<dynamic>?)
?.map(
(e) => PurchasingIngredientDataDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
vendorData: (json['vendor_data'] as List<dynamic>?)
?.map((e) => PurchasingVendorDataDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$PurchasingAnalyticDtoImplToJson(
_$PurchasingAnalyticDtoImpl instance,
) => <String, dynamic>{
'organization_id': instance.organizationId,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'date_from': instance.dateFrom?.toIso8601String(),
'date_to': instance.dateTo?.toIso8601String(),
'group_by': instance.groupBy,
'summary': instance.summary,
'data': instance.data,
'ingredient_data': instance.ingredientData,
'vendor_data': instance.vendorData,
};
_$PurchasingAnalyticSummaryDtoImpl _$$PurchasingAnalyticSummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticSummaryDtoImpl(
totalPurchases: json['total_purchases'] as num?,
totalPurchaseOrders: json['total_purchase_orders'] as num?,
totalQuantity: json['total_quantity'] as num?,
averagePurchaseOrderValue: json['average_purchase_order_value'] as num?,
totalIngredients: json['total_ingredients'] as num?,
totalVendors: json['total_vendors'] as num?,
);
Map<String, dynamic> _$$PurchasingAnalyticSummaryDtoImplToJson(
_$PurchasingAnalyticSummaryDtoImpl instance,
) => <String, dynamic>{
'total_purchases': instance.totalPurchases,
'total_purchase_orders': instance.totalPurchaseOrders,
'total_quantity': instance.totalQuantity,
'average_purchase_order_value': instance.averagePurchaseOrderValue,
'total_ingredients': instance.totalIngredients,
'total_vendors': instance.totalVendors,
};
_$PurchasingAnalyticDataDtoImpl _$$PurchasingAnalyticDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingAnalyticDataDtoImpl(
date: json['date'] == null ? null : DateTime.parse(json['date'] as String),
purchases: json['purchases'] as num?,
purchaseOrders: json['purchase_orders'] as num?,
quantity: json['quantity'] as num?,
ingredients: json['ingredients'] as num?,
vendors: json['vendors'] as num?,
);
Map<String, dynamic> _$$PurchasingAnalyticDataDtoImplToJson(
_$PurchasingAnalyticDataDtoImpl instance,
) => <String, dynamic>{
'date': instance.date?.toIso8601String(),
'purchases': instance.purchases,
'purchase_orders': instance.purchaseOrders,
'quantity': instance.quantity,
'ingredients': instance.ingredients,
'vendors': instance.vendors,
};
_$PurchasingIngredientDataDtoImpl _$$PurchasingIngredientDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingIngredientDataDtoImpl(
ingredientId: json['ingredient_id'] as String?,
ingredientName: json['ingredient_name'] as String?,
quantity: json['quantity'] as num?,
totalCost: json['total_cost'] as num?,
averageUnitCost: json['average_unit_cost'] as num?,
purchaseOrderCount: json['purchase_order_count'] as num?,
);
Map<String, dynamic> _$$PurchasingIngredientDataDtoImplToJson(
_$PurchasingIngredientDataDtoImpl instance,
) => <String, dynamic>{
'ingredient_id': instance.ingredientId,
'ingredient_name': instance.ingredientName,
'quantity': instance.quantity,
'total_cost': instance.totalCost,
'average_unit_cost': instance.averageUnitCost,
'purchase_order_count': instance.purchaseOrderCount,
};
_$PurchasingVendorDataDtoImpl _$$PurchasingVendorDataDtoImplFromJson(
Map<String, dynamic> json,
) => _$PurchasingVendorDataDtoImpl(
vendorId: json['vendor_id'] as String?,
vendorName: json['vendor_name'] as String?,
totalCost: json['total_cost'] as num?,
purchaseOrderCount: json['purchase_order_count'] as num?,
ingredientCount: json['ingredient_count'] as num?,
quantity: json['quantity'] as num?,
);
Map<String, dynamic> _$$PurchasingVendorDataDtoImplToJson(
_$PurchasingVendorDataDtoImpl instance,
) => <String, dynamic>{
'vendor_id': instance.vendorId,
'vendor_name': instance.vendorName,
'total_cost': instance.totalCost,
'purchase_order_count': instance.purchaseOrderCount,
'ingredient_count': instance.ingredientCount,
'quantity': instance.quantity,
};
@@ -231,4 +231,35 @@ class AnalyticRemoteDataProvider {
return DC.error(AnalyticFailure.serverError(e)); return DC.error(AnalyticFailure.serverError(e));
} }
} }
Future<DC<AnalyticFailure, PurchasingAnalyticDto>> fetchPurchasing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) async {
try {
final Map<String, dynamic> params = {
'date_from': dateFrom.toServerDate,
'date_to': dateTo.toServerDate,
};
if (outletId != null) params['outlet_id'] = outletId;
final response = await _apiClient.get(
ApiPath.purchasingAnalytic,
params: params,
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(AnalyticFailure.empty());
}
final dto = PurchasingAnalyticDto.fromJson(response.data['data']);
return DC.data(dto);
} on ApiFailure catch (e, s) {
log('fetchPurchasingError', name: _logName, error: e, stackTrace: s);
return DC.error(AnalyticFailure.serverError(e));
}
}
} }
@@ -0,0 +1,143 @@
part of '../analytic_dtos.dart';
@freezed
class PurchasingAnalyticDto with _$PurchasingAnalyticDto {
const PurchasingAnalyticDto._();
const factory PurchasingAnalyticDto({
@JsonKey(name: 'organization_id') String? organizationId,
@JsonKey(name: 'outlet_id') String? outletId,
@JsonKey(name: 'outlet_name') String? outletName,
@JsonKey(name: 'date_from') DateTime? dateFrom,
@JsonKey(name: 'date_to') DateTime? dateTo,
@JsonKey(name: 'group_by') String? groupBy,
@JsonKey(name: 'summary') PurchasingAnalyticSummaryDto? summary,
@JsonKey(name: 'data') List<PurchasingAnalyticDataDto>? data,
@JsonKey(name: 'ingredient_data')
List<PurchasingIngredientDataDto>? ingredientData,
@JsonKey(name: 'vendor_data') List<PurchasingVendorDataDto>? vendorData,
}) = _PurchasingAnalyticDto;
factory PurchasingAnalyticDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticDtoFromJson(json);
PurchasingAnalytic toDomain() => PurchasingAnalytic(
organizationId: organizationId ?? '',
outletId: outletId ?? '',
outletName: outletName ?? '',
dateFrom: dateFrom ?? DateTime.fromMillisecondsSinceEpoch(0),
dateTo: dateTo ?? DateTime.fromMillisecondsSinceEpoch(0),
groupBy: groupBy ?? '',
summary: summary?.toDomain() ?? PurchasingAnalyticSummary.empty(),
data: data?.map((e) => e.toDomain()).toList() ?? [],
ingredientData:
ingredientData?.map((e) => e.toDomain()).toList() ?? [],
vendorData: vendorData?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
class PurchasingAnalyticSummaryDto with _$PurchasingAnalyticSummaryDto {
const PurchasingAnalyticSummaryDto._();
const factory PurchasingAnalyticSummaryDto({
@JsonKey(name: 'total_purchases') num? totalPurchases,
@JsonKey(name: 'total_purchase_orders') num? totalPurchaseOrders,
@JsonKey(name: 'total_quantity') num? totalQuantity,
@JsonKey(name: 'average_purchase_order_value')
num? averagePurchaseOrderValue,
@JsonKey(name: 'total_ingredients') num? totalIngredients,
@JsonKey(name: 'total_vendors') num? totalVendors,
}) = _PurchasingAnalyticSummaryDto;
factory PurchasingAnalyticSummaryDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticSummaryDtoFromJson(json);
PurchasingAnalyticSummary toDomain() => PurchasingAnalyticSummary(
totalPurchases: totalPurchases?.toInt() ?? 0,
totalPurchaseOrders: totalPurchaseOrders?.toInt() ?? 0,
totalQuantity: totalQuantity?.toInt() ?? 0,
averagePurchaseOrderValue:
averagePurchaseOrderValue?.toDouble() ?? 0,
totalIngredients: totalIngredients?.toInt() ?? 0,
totalVendors: totalVendors?.toInt() ?? 0,
);
}
@freezed
class PurchasingAnalyticDataDto with _$PurchasingAnalyticDataDto {
const PurchasingAnalyticDataDto._();
const factory PurchasingAnalyticDataDto({
@JsonKey(name: 'date') DateTime? date,
@JsonKey(name: 'purchases') num? purchases,
@JsonKey(name: 'purchase_orders') num? purchaseOrders,
@JsonKey(name: 'quantity') num? quantity,
@JsonKey(name: 'ingredients') num? ingredients,
@JsonKey(name: 'vendors') num? vendors,
}) = _PurchasingAnalyticDataDto;
factory PurchasingAnalyticDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingAnalyticDataDtoFromJson(json);
PurchasingAnalyticData toDomain() => PurchasingAnalyticData(
date: date ?? DateTime.fromMillisecondsSinceEpoch(0),
purchases: purchases?.toInt() ?? 0,
purchaseOrders: purchaseOrders?.toInt() ?? 0,
quantity: quantity?.toInt() ?? 0,
ingredients: ingredients?.toInt() ?? 0,
vendors: vendors?.toInt() ?? 0,
);
}
@freezed
class PurchasingIngredientDataDto with _$PurchasingIngredientDataDto {
const PurchasingIngredientDataDto._();
const factory PurchasingIngredientDataDto({
@JsonKey(name: 'ingredient_id') String? ingredientId,
@JsonKey(name: 'ingredient_name') String? ingredientName,
@JsonKey(name: 'quantity') num? quantity,
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'average_unit_cost') num? averageUnitCost,
@JsonKey(name: 'purchase_order_count') num? purchaseOrderCount,
}) = _PurchasingIngredientDataDto;
factory PurchasingIngredientDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingIngredientDataDtoFromJson(json);
PurchasingIngredientData toDomain() => PurchasingIngredientData(
ingredientId: ingredientId ?? '',
ingredientName: ingredientName ?? '',
quantity: quantity?.toInt() ?? 0,
totalCost: totalCost?.toInt() ?? 0,
averageUnitCost: averageUnitCost?.toDouble() ?? 0,
purchaseOrderCount: purchaseOrderCount?.toInt() ?? 0,
);
}
@freezed
class PurchasingVendorDataDto with _$PurchasingVendorDataDto {
const PurchasingVendorDataDto._();
const factory PurchasingVendorDataDto({
@JsonKey(name: 'vendor_id') String? vendorId,
@JsonKey(name: 'vendor_name') String? vendorName,
@JsonKey(name: 'total_cost') num? totalCost,
@JsonKey(name: 'purchase_order_count') num? purchaseOrderCount,
@JsonKey(name: 'ingredient_count') num? ingredientCount,
@JsonKey(name: 'quantity') num? quantity,
}) = _PurchasingVendorDataDto;
factory PurchasingVendorDataDto.fromJson(Map<String, dynamic> json) =>
_$PurchasingVendorDataDtoFromJson(json);
PurchasingVendorData toDomain() => PurchasingVendorData(
vendorId: vendorId ?? '',
vendorName: vendorName ?? '',
totalCost: totalCost?.toInt() ?? 0,
purchaseOrderCount: purchaseOrderCount?.toInt() ?? 0,
ingredientCount: ingredientCount?.toInt() ?? 0,
quantity: quantity?.toInt() ?? 0,
);
}
@@ -176,4 +176,25 @@ class AnalyticRepository implements IAnalyticRepository {
return left(const AnalyticFailure.unexpectedError()); return left(const AnalyticFailure.unexpectedError());
} }
} }
@override
Future<Either<AnalyticFailure, PurchasingAnalytic>> getPurchasing({
required DateTime dateFrom,
required DateTime dateTo,
String? outletId,
}) async {
try {
final result = await _dataProvider.fetchPurchasing(
dateFrom: dateFrom,
dateTo: dateTo,
outletId: _resolveOutletId(outletId),
);
if (result.hasError) return left(result.error!);
return right(result.data!.toDomain());
} catch (e, s) {
log('getPurchasingError', name: _logName, error: e, stackTrace: s);
return left(const AnalyticFailure.unexpectedError());
}
}
} }
+5
View File
@@ -21,6 +21,8 @@ import 'package:apskel_owner_flutter/application/analytic/product_analytic_loade
as _i221; as _i221;
import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart'
as _i11; as _i11;
import 'package:apskel_owner_flutter/application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart'
as _i755;
import 'package:apskel_owner_flutter/application/analytic/sales_loader/sales_loader_bloc.dart' import 'package:apskel_owner_flutter/application/analytic/sales_loader/sales_loader_bloc.dart'
as _i889; as _i889;
import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945; import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945;
@@ -248,6 +250,9 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i850.OutletLocalDataProvider>(), gh<_i850.OutletLocalDataProvider>(),
), ),
); );
gh.factory<_i755.PurchasingAnalyticLoaderBloc>(
() => _i755.PurchasingAnalyticLoaderBloc(gh<_i477.IAnalyticRepository>()),
);
gh.factory<_i889.SalesLoaderBloc>( gh.factory<_i889.SalesLoaderBloc>(
() => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()),
); );
+17 -5
View File
@@ -1,3 +1,4 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -25,12 +26,23 @@ void main() async {
kReleaseMode ? Environment.prod : Environment.dev, kReleaseMode ? Environment.prod : Environment.dev,
); );
// Setup Crashlytics
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
// Initialize FCM after dependencies are ready // Initialize FCM after dependencies are ready
await getIt<FcmService>().initialize( try {
onMessageTap: (message) { await getIt<FcmService>().initialize(
debugPrint('[FCM] Navigate based on: ${message.data}'); onMessageTap: (message) {
}, debugPrint('[FCM] Navigate based on: ${message.data}');
); },
);
} catch (e) {
debugPrint('[FCM] Initialization failed: $e');
}
runApp(const AppWidget()); runApp(const AppWidget());
} }
@@ -56,6 +56,11 @@ class HomeFeature extends StatelessWidget {
iconPath: Assets.icons.icReportProduct.path, iconPath: Assets.icons.icReportProduct.path,
onTap: () => context.router.push(ProductAnalyticRoute()), onTap: () => context.router.push(ProductAnalyticRoute()),
), ),
// HomeFeatureTile(
// title: context.lang.inventory,
// iconPath: Assets.icons.icReportProduct.path,
// onTap: () => context.router.push(InventoryRoute()),
// ),
], ],
), ),
// Row( // Row(
+540 -141
View File
@@ -1,51 +1,70 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.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/extension/extension.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../injection.dart';
import '../../components/appbar/appbar.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/stat_card.dart';
import 'widgets/status_chip.dart'; import 'widgets/vendor_card.dart';
@RoutePage() @RoutePage()
class PurchasePage extends StatefulWidget { class PurchasePage extends StatefulWidget implements AutoRouteWrapper {
const PurchasePage({super.key}); const PurchasePage({super.key});
@override @override
State<PurchasePage> createState() => _PurchasePageState(); 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> class _PurchasePageState extends State<PurchasePage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late AnimationController cardAnimation; late AnimationController _fadeController;
String selectedFilter = 'Semua'; late Animation<double> _fadeAnimation;
final List<String> filterOptions = [
'Semua',
'Pending',
'Completed',
'Cancelled',
];
final List<Map<String, dynamic>> purchaseData = [
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fadeController = AnimationController(
cardAnimation = AnimationController( duration: const Duration(milliseconds: 600),
duration: const Duration(milliseconds: 1200),
vsync: this, vsync: this,
); );
_fadeAnimation = CurvedAnimation(
cardAnimation.forward(); parent: _fadeController,
curve: Curves.easeOut,
);
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) _fadeController.forward();
});
} }
@override @override
void dispose() { void dispose() {
cardAnimation.dispose(); _fadeController.dispose();
super.dispose(); super.dispose();
} }
@@ -53,141 +72,521 @@ class _PurchasePageState extends State<PurchasePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColor.background, backgroundColor: AppColor.background,
body: CustomScrollView( body: MultiBlocListener(
slivers: [ listeners: [
SliverAppBar( // Re-fetch when date range changes
expandedHeight: 120.0, BlocListener<PurchasingAnalyticLoaderBloc,
floating: false, PurchasingAnalyticLoaderState>(
pinned: true, listenWhen: (prev, curr) =>
elevation: 0, prev.dateFrom != curr.dateFrom ||
backgroundColor: AppColor.primary, prev.dateTo != curr.dateTo,
listener: (context, _) => context
flexibleSpace: CustomAppBar(title: context.lang.purchase), .read<PurchasingAnalyticLoaderBloc>()
.add(const PurchasingAnalyticLoaderEvent.fetched()),
), ),
// Re-fetch when outlet changes
// Stats Cards BlocListener<PurchasingAnalyticLoaderBloc,
SliverToBoxAdapter( PurchasingAnalyticLoaderState>(
child: Container( listenWhen: (prev, curr) => prev.outletId != curr.outletId,
color: AppColor.background, listener: (context, _) => context
padding: const EdgeInsets.all(16.0), .read<PurchasingAnalyticLoaderBloc>()
child: Row( .add(const PurchasingAnalyticLoaderEvent.fetched()),
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,
),
),
],
),
),
), ),
],
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 // Date Range + Outlet Picker
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: FadeTransition(
color: AppColor.surface, opacity: _fadeAnimation,
padding: const EdgeInsets.all(16.0), child: Padding(
child: Column( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ children: [
Row( DateRangePickerField(
mainAxisAlignment: MainAxisAlignment.spaceBetween, maxDate: DateTime.now(),
children: [ startDate: state.dateFrom,
Text( endDate: state.dateTo,
context.lang.history_purchase, onChanged: (startDate, endDate) {
style: AppStyle.lg.copyWith( context
fontWeight: FontWeight.w600, .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, const SliverToBoxAdapter(child: SpaceHeight(16)),
vertical: 6,
), // Summary Section
decoration: BoxDecoration( SliverToBoxAdapter(
color: AppColor.primary, child: FadeTransition(
borderRadius: BorderRadius.circular(20), opacity: _fadeAnimation,
), child: Padding(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 16),
'${purchaseData.length} ${context.lang.orders}', child: Column(
style: AppStyle.sm.copyWith( crossAxisAlignment: CrossAxisAlignment.start,
color: AppColor.textWhite, 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( Column(
height: 40, crossAxisAlignment: CrossAxisAlignment.end,
child: ListView.builder( children: [
scrollDirection: Axis.horizontal, Text(
itemCount: filterOptions.length, '${state.purchasing.summary.totalPurchaseOrders} PO',
itemBuilder: (context, index) { style: const TextStyle(
final isSelected = color: AppColor.textWhite,
selectedFilter == filterOptions[index]; fontSize: 16,
return PurchaseStatusChip( fontWeight: FontWeight.bold,
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),
), ),
); ),
}, Text(
); 'purchase order',
}, childCount: purchaseData.length), style: TextStyle(
color: AppColor.textWhite.withOpacity(0.8),
fontSize: 12,
),
),
],
),
],
), ),
), ),
);
},
);
}
// Bottom spacing for FAB // ─── Lists ────────────────────────────────────────────────────────────────
const SliverToBoxAdapter(child: SizedBox(height: 80)),
], 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),
),
), ),
); );
} }
@@ -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),
],
),
);
}
}
@@ -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 IconData icon;
final Color iconColor; final Color iconColor;
final Animation<double> cardAnimation; final Animation<double> cardAnimation;
const PurchaseStatCard({ const PurchaseStatCard({
super.key, super.key,
required this.title, required this.title,
@@ -25,65 +26,41 @@ class PurchaseStatCard extends StatelessWidget {
return Transform.scale( return Transform.scale(
scale: 0.8 + (cardAnimation.value * 0.2), scale: 0.8 + (cardAnimation.value * 0.2),
child: Opacity( child: Opacity(
opacity: cardAnimation.value, opacity: cardAnimation.value.clamp(0.0, 1.0),
child: Container( child: Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: AppColor.surface,
colors: [AppColor.surface, AppColor.surface.withOpacity(0.9)], borderRadius: BorderRadius.circular(16),
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: AppColor.primary.withOpacity(0.1), color: AppColor.primary.withOpacity(0.08),
blurRadius: 20, blurRadius: 16,
offset: const Offset(0, 8), offset: const Offset(0, 6),
spreadRadius: 0,
), ),
BoxShadow( BoxShadow(
color: AppColor.black.withOpacity(0.05), color: AppColor.black.withOpacity(0.04),
blurRadius: 10, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
], ],
border: Border.all( border: Border.all(
color: AppColor.border.withOpacity(0.3), color: AppColor.border.withOpacity(0.25),
width: 1, width: 1,
), ),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Container(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.all(10),
children: [ decoration: BoxDecoration(
Container( color: iconColor.withOpacity(0.12),
padding: const EdgeInsets.all(12), borderRadius: BorderRadius.circular(10),
decoration: BoxDecoration( ),
gradient: LinearGradient( child: Icon(icon, color: iconColor, size: 22),
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),
),
],
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
Text( Text(
title, title,
style: AppStyle.sm.copyWith( style: AppStyle.sm.copyWith(
@@ -91,14 +68,16 @@ class PurchaseStatCard extends StatelessWidget {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 4),
Text( Text(
value, value,
style: AppStyle.lg.copyWith( style: AppStyle.md.copyWith(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: AppColor.textPrimary, color: AppColor.textPrimary,
height: 1.2, 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,
),
);
}
}
@@ -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,
),
),
);
}
}
+1 -1
View File
@@ -501,7 +501,7 @@ class PurchaseRoute extends _i26.PageRouteInfo<void> {
static _i26.PageInfo page = _i26.PageInfo( static _i26.PageInfo page = _i26.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i21.PurchasePage(); return _i26.WrappedRoute(child: const _i21.PurchasePage());
}, },
); );
} }
@@ -9,6 +9,7 @@ import connectivity_plus
import device_info_plus import device_info_plus
import file_selector_macos import file_selector_macos
import firebase_core import firebase_core
import firebase_crashlytics
import firebase_messaging import firebase_messaging
import flutter_local_notifications import flutter_local_notifications
import open_file_mac import open_file_mac
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
+35 -19
View File
@@ -213,10 +213,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -481,6 +481,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.24.0" version: "2.24.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "9897c01efaa950d2f6da8317d12452749a74dc45f33b46390a14cfe28067f271"
url: "https://pub.dev"
source: hosted
version: "3.5.7"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "16a71e08fbf6e00382816e1b13397898c29a54fa0ad969c2c2a3b82a704877f0"
url: "https://pub.dev"
source: hosted
version: "3.6.35"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -865,26 +881,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.9" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
line_icons: line_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -921,26 +937,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -1510,10 +1526,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.10"
time: time:
dependency: transitive dependency: transitive
description: description:
@@ -1646,10 +1662,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1731,5 +1747,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"
+2 -1
View File
@@ -3,7 +3,7 @@ description: "A new Flutter project."
publish_to: "none" publish_to: "none"
version: 1.0.0+1 version: 1.0.1+2
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -50,6 +50,7 @@ dependencies:
permission_handler: ^12.0.1 permission_handler: ^12.0.1
firebase_core: 2.32.0 firebase_core: 2.32.0
firebase_messaging: 14.7.10 firebase_messaging: 14.7.10
firebase_crashlytics: ^3.5.7
flutter_local_notifications: ^18.0.1 flutter_local_notifications: ^18.0.1
hugeicons: ^1.1.6 hugeicons: ^1.1.6