From 1aa65d17326d1d4a3278f520594a58f0bd0680b2 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 19 Aug 2025 17:05:55 +0700 Subject: [PATCH] feat: inventory report --- android/app/src/main/AndroidManifest.xml | 5 + .../inventory_report_bloc.dart | 66 ++ .../inventory_report_bloc.freezed.dart | 645 ++++++++++++++++++ .../inventory_report_event.dart | 10 + .../inventory_report_state.dart | 20 + lib/common/utils/pdf_service.dart | 79 +++ lib/common/utils/permission.dart | 72 ++ lib/injection.config.dart | 8 + .../components/report/inventory_report.dart | 547 +++++++++++++++ .../pages/download/download_report_page.dart | 268 +++++--- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 144 ++++ pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 17 files changed, 1772 insertions(+), 106 deletions(-) create mode 100644 lib/application/report/inventory_report/inventory_report_bloc.dart create mode 100644 lib/application/report/inventory_report/inventory_report_bloc.freezed.dart create mode 100644 lib/application/report/inventory_report/inventory_report_event.dart create mode 100644 lib/application/report/inventory_report/inventory_report_state.dart create mode 100644 lib/common/utils/pdf_service.dart create mode 100644 lib/common/utils/permission.dart create mode 100644 lib/presentation/components/report/inventory_report.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3d9eb2d..0a0b209 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,10 @@ + + + + + { + final IAnalyticRepository _analyticRepository; + final IOutletRepository _outletRepository; + InventoryReportBloc(this._analyticRepository, this._outletRepository) + : super(InventoryReportState.initial()) { + on(_onInventoryReportEvent); + } + + Future _onInventoryReportEvent( + InventoryReportEvent event, + Emitter emit, + ) { + return event.map( + fetchedOutlet: (e) async { + emit( + state.copyWith(isFetchingOutlet: true, failureOptionOutlet: none()), + ); + + final result = await _outletRepository.currentOutlet(); + + var data = result.fold( + (f) => state.copyWith(failureOptionOutlet: optionOf(f)), + (currentOutlet) => state.copyWith(outlet: currentOutlet), + ); + + emit(data.copyWith(isFetchingOutlet: false)); + }, + fetchedInventory: (e) async { + emit( + state.copyWith( + isFetching: true, + failureOptionInventoryAnalytic: none(), + ), + ); + + final result = await _analyticRepository.getInventory( + dateFrom: e.dateFrom, + dateTo: e.dateTo, + ); + + var data = result.fold( + (f) => state.copyWith(failureOptionInventoryAnalytic: optionOf(f)), + (inventoryAnalytic) => + state.copyWith(inventoryAnalytic: inventoryAnalytic), + ); + + emit(data.copyWith(isFetching: false)); + }, + ); + } +} diff --git a/lib/application/report/inventory_report/inventory_report_bloc.freezed.dart b/lib/application/report/inventory_report/inventory_report_bloc.freezed.dart new file mode 100644 index 0000000..714993c --- /dev/null +++ b/lib/application/report/inventory_report/inventory_report_bloc.freezed.dart @@ -0,0 +1,645 @@ +// 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 'inventory_report_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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 _$InventoryReportEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedInventory, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedInventory value) fetchedInventory, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedInventory value)? fetchedInventory, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedInventory value)? fetchedInventory, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $InventoryReportEventCopyWith<$Res> { + factory $InventoryReportEventCopyWith( + InventoryReportEvent value, + $Res Function(InventoryReportEvent) then, + ) = _$InventoryReportEventCopyWithImpl<$Res, InventoryReportEvent>; +} + +/// @nodoc +class _$InventoryReportEventCopyWithImpl< + $Res, + $Val extends InventoryReportEvent +> + implements $InventoryReportEventCopyWith<$Res> { + _$InventoryReportEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of InventoryReportEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$FetchedOutletImplCopyWith<$Res> { + factory _$$FetchedOutletImplCopyWith( + _$FetchedOutletImpl value, + $Res Function(_$FetchedOutletImpl) then, + ) = __$$FetchedOutletImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$FetchedOutletImplCopyWithImpl<$Res> + extends _$InventoryReportEventCopyWithImpl<$Res, _$FetchedOutletImpl> + implements _$$FetchedOutletImplCopyWith<$Res> { + __$$FetchedOutletImplCopyWithImpl( + _$FetchedOutletImpl _value, + $Res Function(_$FetchedOutletImpl) _then, + ) : super(_value, _then); + + /// Create a copy of InventoryReportEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$FetchedOutletImpl implements _FetchedOutlet { + const _$FetchedOutletImpl(); + + @override + String toString() { + return 'InventoryReportEvent.fetchedOutlet()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$FetchedOutletImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedInventory, + }) { + return fetchedOutlet(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + }) { + return fetchedOutlet?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + required TResult orElse(), + }) { + if (fetchedOutlet != null) { + return fetchedOutlet(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedInventory value) fetchedInventory, + }) { + return fetchedOutlet(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedInventory value)? fetchedInventory, + }) { + return fetchedOutlet?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedInventory value)? fetchedInventory, + required TResult orElse(), + }) { + if (fetchedOutlet != null) { + return fetchedOutlet(this); + } + return orElse(); + } +} + +abstract class _FetchedOutlet implements InventoryReportEvent { + const factory _FetchedOutlet() = _$FetchedOutletImpl; +} + +/// @nodoc +abstract class _$$FetchedInventoryImplCopyWith<$Res> { + factory _$$FetchedInventoryImplCopyWith( + _$FetchedInventoryImpl value, + $Res Function(_$FetchedInventoryImpl) then, + ) = __$$FetchedInventoryImplCopyWithImpl<$Res>; + @useResult + $Res call({DateTime dateFrom, DateTime dateTo}); +} + +/// @nodoc +class __$$FetchedInventoryImplCopyWithImpl<$Res> + extends _$InventoryReportEventCopyWithImpl<$Res, _$FetchedInventoryImpl> + implements _$$FetchedInventoryImplCopyWith<$Res> { + __$$FetchedInventoryImplCopyWithImpl( + _$FetchedInventoryImpl _value, + $Res Function(_$FetchedInventoryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of InventoryReportEvent + /// 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( + _$FetchedInventoryImpl( + 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 _$FetchedInventoryImpl implements _FetchedInventory { + const _$FetchedInventoryImpl(this.dateFrom, this.dateTo); + + @override + final DateTime dateFrom; + @override + final DateTime dateTo; + + @override + String toString() { + return 'InventoryReportEvent.fetchedInventory(dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FetchedInventoryImpl && + (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 InventoryReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FetchedInventoryImplCopyWith<_$FetchedInventoryImpl> get copyWith => + __$$FetchedInventoryImplCopyWithImpl<_$FetchedInventoryImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedInventory, + }) { + return fetchedInventory(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + }) { + return fetchedInventory?.call(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedInventory, + required TResult orElse(), + }) { + if (fetchedInventory != null) { + return fetchedInventory(dateFrom, dateTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedInventory value) fetchedInventory, + }) { + return fetchedInventory(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedInventory value)? fetchedInventory, + }) { + return fetchedInventory?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedInventory value)? fetchedInventory, + required TResult orElse(), + }) { + if (fetchedInventory != null) { + return fetchedInventory(this); + } + return orElse(); + } +} + +abstract class _FetchedInventory implements InventoryReportEvent { + const factory _FetchedInventory( + final DateTime dateFrom, + final DateTime dateTo, + ) = _$FetchedInventoryImpl; + + DateTime get dateFrom; + DateTime get dateTo; + + /// Create a copy of InventoryReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FetchedInventoryImplCopyWith<_$FetchedInventoryImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$InventoryReportState { + InventoryAnalytic get inventoryAnalytic => throw _privateConstructorUsedError; + Option get failureOptionInventoryAnalytic => + throw _privateConstructorUsedError; + Outlet get outlet => throw _privateConstructorUsedError; + Option get failureOptionOutlet => + throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + bool get isFetchingOutlet => throw _privateConstructorUsedError; + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $InventoryReportStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $InventoryReportStateCopyWith<$Res> { + factory $InventoryReportStateCopyWith( + InventoryReportState value, + $Res Function(InventoryReportState) then, + ) = _$InventoryReportStateCopyWithImpl<$Res, InventoryReportState>; + @useResult + $Res call({ + InventoryAnalytic inventoryAnalytic, + Option failureOptionInventoryAnalytic, + Outlet outlet, + Option failureOptionOutlet, + bool isFetching, + bool isFetchingOutlet, + }); + + $InventoryAnalyticCopyWith<$Res> get inventoryAnalytic; + $OutletCopyWith<$Res> get outlet; +} + +/// @nodoc +class _$InventoryReportStateCopyWithImpl< + $Res, + $Val extends InventoryReportState +> + implements $InventoryReportStateCopyWith<$Res> { + _$InventoryReportStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? inventoryAnalytic = null, + Object? failureOptionInventoryAnalytic = null, + Object? outlet = null, + Object? failureOptionOutlet = null, + Object? isFetching = null, + Object? isFetchingOutlet = null, + }) { + return _then( + _value.copyWith( + inventoryAnalytic: null == inventoryAnalytic + ? _value.inventoryAnalytic + : inventoryAnalytic // ignore: cast_nullable_to_non_nullable + as InventoryAnalytic, + failureOptionInventoryAnalytic: + null == failureOptionInventoryAnalytic + ? _value.failureOptionInventoryAnalytic + : failureOptionInventoryAnalytic // ignore: cast_nullable_to_non_nullable + as Option, + outlet: null == outlet + ? _value.outlet + : outlet // ignore: cast_nullable_to_non_nullable + as Outlet, + failureOptionOutlet: null == failureOptionOutlet + ? _value.failureOptionOutlet + : failureOptionOutlet // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + isFetchingOutlet: null == isFetchingOutlet + ? _value.isFetchingOutlet + : isFetchingOutlet // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $InventoryAnalyticCopyWith<$Res> get inventoryAnalytic { + return $InventoryAnalyticCopyWith<$Res>(_value.inventoryAnalytic, (value) { + return _then(_value.copyWith(inventoryAnalytic: value) as $Val); + }); + } + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OutletCopyWith<$Res> get outlet { + return $OutletCopyWith<$Res>(_value.outlet, (value) { + return _then(_value.copyWith(outlet: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$InventoryReportStateImplCopyWith<$Res> + implements $InventoryReportStateCopyWith<$Res> { + factory _$$InventoryReportStateImplCopyWith( + _$InventoryReportStateImpl value, + $Res Function(_$InventoryReportStateImpl) then, + ) = __$$InventoryReportStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + InventoryAnalytic inventoryAnalytic, + Option failureOptionInventoryAnalytic, + Outlet outlet, + Option failureOptionOutlet, + bool isFetching, + bool isFetchingOutlet, + }); + + @override + $InventoryAnalyticCopyWith<$Res> get inventoryAnalytic; + @override + $OutletCopyWith<$Res> get outlet; +} + +/// @nodoc +class __$$InventoryReportStateImplCopyWithImpl<$Res> + extends _$InventoryReportStateCopyWithImpl<$Res, _$InventoryReportStateImpl> + implements _$$InventoryReportStateImplCopyWith<$Res> { + __$$InventoryReportStateImplCopyWithImpl( + _$InventoryReportStateImpl _value, + $Res Function(_$InventoryReportStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? inventoryAnalytic = null, + Object? failureOptionInventoryAnalytic = null, + Object? outlet = null, + Object? failureOptionOutlet = null, + Object? isFetching = null, + Object? isFetchingOutlet = null, + }) { + return _then( + _$InventoryReportStateImpl( + inventoryAnalytic: null == inventoryAnalytic + ? _value.inventoryAnalytic + : inventoryAnalytic // ignore: cast_nullable_to_non_nullable + as InventoryAnalytic, + failureOptionInventoryAnalytic: null == failureOptionInventoryAnalytic + ? _value.failureOptionInventoryAnalytic + : failureOptionInventoryAnalytic // ignore: cast_nullable_to_non_nullable + as Option, + outlet: null == outlet + ? _value.outlet + : outlet // ignore: cast_nullable_to_non_nullable + as Outlet, + failureOptionOutlet: null == failureOptionOutlet + ? _value.failureOptionOutlet + : failureOptionOutlet // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + isFetchingOutlet: null == isFetchingOutlet + ? _value.isFetchingOutlet + : isFetchingOutlet // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$InventoryReportStateImpl implements _InventoryReportState { + const _$InventoryReportStateImpl({ + required this.inventoryAnalytic, + required this.failureOptionInventoryAnalytic, + required this.outlet, + required this.failureOptionOutlet, + this.isFetching = false, + this.isFetchingOutlet = false, + }); + + @override + final InventoryAnalytic inventoryAnalytic; + @override + final Option failureOptionInventoryAnalytic; + @override + final Outlet outlet; + @override + final Option failureOptionOutlet; + @override + @JsonKey() + final bool isFetching; + @override + @JsonKey() + final bool isFetchingOutlet; + + @override + String toString() { + return 'InventoryReportState(inventoryAnalytic: $inventoryAnalytic, failureOptionInventoryAnalytic: $failureOptionInventoryAnalytic, outlet: $outlet, failureOptionOutlet: $failureOptionOutlet, isFetching: $isFetching, isFetchingOutlet: $isFetchingOutlet)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$InventoryReportStateImpl && + (identical(other.inventoryAnalytic, inventoryAnalytic) || + other.inventoryAnalytic == inventoryAnalytic) && + (identical( + other.failureOptionInventoryAnalytic, + failureOptionInventoryAnalytic, + ) || + other.failureOptionInventoryAnalytic == + failureOptionInventoryAnalytic) && + (identical(other.outlet, outlet) || other.outlet == outlet) && + (identical(other.failureOptionOutlet, failureOptionOutlet) || + other.failureOptionOutlet == failureOptionOutlet) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching) && + (identical(other.isFetchingOutlet, isFetchingOutlet) || + other.isFetchingOutlet == isFetchingOutlet)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + inventoryAnalytic, + failureOptionInventoryAnalytic, + outlet, + failureOptionOutlet, + isFetching, + isFetchingOutlet, + ); + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$InventoryReportStateImplCopyWith<_$InventoryReportStateImpl> + get copyWith => + __$$InventoryReportStateImplCopyWithImpl<_$InventoryReportStateImpl>( + this, + _$identity, + ); +} + +abstract class _InventoryReportState implements InventoryReportState { + const factory _InventoryReportState({ + required final InventoryAnalytic inventoryAnalytic, + required final Option failureOptionInventoryAnalytic, + required final Outlet outlet, + required final Option failureOptionOutlet, + final bool isFetching, + final bool isFetchingOutlet, + }) = _$InventoryReportStateImpl; + + @override + InventoryAnalytic get inventoryAnalytic; + @override + Option get failureOptionInventoryAnalytic; + @override + Outlet get outlet; + @override + Option get failureOptionOutlet; + @override + bool get isFetching; + @override + bool get isFetchingOutlet; + + /// Create a copy of InventoryReportState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$InventoryReportStateImplCopyWith<_$InventoryReportStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/application/report/inventory_report/inventory_report_event.dart b/lib/application/report/inventory_report/inventory_report_event.dart new file mode 100644 index 0000000..713335f --- /dev/null +++ b/lib/application/report/inventory_report/inventory_report_event.dart @@ -0,0 +1,10 @@ +part of 'inventory_report_bloc.dart'; + +@freezed +class InventoryReportEvent with _$InventoryReportEvent { + const factory InventoryReportEvent.fetchedOutlet() = _FetchedOutlet; + const factory InventoryReportEvent.fetchedInventory( + DateTime dateFrom, + DateTime dateTo, + ) = _FetchedInventory; +} diff --git a/lib/application/report/inventory_report/inventory_report_state.dart b/lib/application/report/inventory_report/inventory_report_state.dart new file mode 100644 index 0000000..fb652ef --- /dev/null +++ b/lib/application/report/inventory_report/inventory_report_state.dart @@ -0,0 +1,20 @@ +part of 'inventory_report_bloc.dart'; + +@freezed +class InventoryReportState with _$InventoryReportState { + const factory InventoryReportState({ + required InventoryAnalytic inventoryAnalytic, + required Option failureOptionInventoryAnalytic, + required Outlet outlet, + required Option failureOptionOutlet, + @Default(false) bool isFetching, + @Default(false) bool isFetchingOutlet, + }) = _InventoryReportState; + + factory InventoryReportState.initial() => InventoryReportState( + inventoryAnalytic: InventoryAnalytic.empty(), + failureOptionInventoryAnalytic: none(), + outlet: Outlet.empty(), + failureOptionOutlet: none(), + ); +} diff --git a/lib/common/utils/pdf_service.dart b/lib/common/utils/pdf_service.dart new file mode 100644 index 0000000..a7222a7 --- /dev/null +++ b/lib/common/utils/pdf_service.dart @@ -0,0 +1,79 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:pdf/widgets.dart'; + +class HelperPdfService { + static Future saveDocument({ + required String name, + required Document pdf, + }) async { + try { + log("Starting PDF save process for: $name"); + log("PDF document object: $pdf"); + + final bytes = await pdf.save(); + log("PDF bytes generated successfully, size: ${bytes.length} bytes"); + + if (bytes.isEmpty) { + log("WARNING: PDF bytes are empty!"); + return Future.error("PDF bytes are empty"); + } + + final dir = await getApplicationDocumentsDirectory(); + log("Documents directory: ${dir.path}"); + + final file = File('${dir.path}/$name'); + log("Saving PDF to: ${file.path}"); + + await file.writeAsBytes(bytes); + log("PDF saved successfully to: ${file.path}"); + + // Verify file was created + if (await file.exists()) { + final fileSize = await file.length(); + log("File exists and size is: $fileSize bytes"); + } else { + log("ERROR: File was not created!"); + return Future.error("File was not created"); + } + + return file; + } catch (e) { + log("Failed to save document: $e"); + log("Error stack trace: ${StackTrace.current}"); + return Future.error("Failed to save document: $e"); + } + } + + static Future openFile(File file) async { + try { + final url = file.path; + log("Attempting to open file: $url"); + + if (!await file.exists()) { + log("ERROR: File does not exist: $url"); + return; + } + + final fileSize = await file.length(); + log("File exists and size is: $fileSize bytes"); + + log("Calling OpenFile.open..."); + final result = await OpenFile.open(url, type: "application/pdf"); + log("OpenFile result: $result"); + + if (result.type == ResultType.done) { + log("File opened successfully"); + } else { + log("File opening failed with result: ${result.type}"); + log("Error message: ${result.message}"); + } + } catch (e) { + log("Failed to open file: $e"); + log("Error stack trace: ${StackTrace.current}"); + } + } +} diff --git a/lib/common/utils/permission.dart b/lib/common/utils/permission.dart new file mode 100644 index 0000000..b2cb177 --- /dev/null +++ b/lib/common/utils/permission.dart @@ -0,0 +1,72 @@ +import 'dart:developer'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class PermessionHelper { + Future checkPermission() async { + final deviceInfo = await DeviceInfoPlugin().androidInfo; + bool permissionStatus; + if (deviceInfo.version.sdkInt > 32) { + permissionStatus = await Permission.photos.request().isGranted; + } else { + permissionStatus = await Permission.storage.request().isGranted; + } + + if (permissionStatus) { + log('Izin penyimpanan sudah diberikan.'); + } else { + if (deviceInfo.version.sdkInt > 32) { + log('deviceInfo.version.sdkInt > 32.'); + permissionStatus = await Permission.photos.request().isGranted; + } else { + permissionStatus = await Permission.storage.request().isGranted; + } + // } else { + // openAppSettings(); + // } + } + log('permissionStatus: $permissionStatus'); + return permissionStatus; + } + + void permessionPrinter() async { + Map statuses = await [ + Permission.bluetooth, + Permission.bluetoothScan, + Permission.bluetoothAdvertise, + Permission.bluetoothConnect, + ].request(); + + log("statuses: $statuses"); + } +} + + // try { + // final status = + // await PermessionHelper().checkPermission(); + // if (status) { + // final pdfFile = await InventoryReport.previewPdf( + // searchDateFormatted: widget.searchDateFormatted, + // inventory: widget.inventory, + // ); + // log("pdfFile: $pdfFile"); + // await HelperPdfService.openFile(pdfFile); + // } else { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text( + // 'Storage permission is required to save PDF'), + // backgroundColor: Colors.red, + // ), + // ); + // } + // } catch (e) { + // log("Error generating PDF: $e"); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text('Failed to generate PDF: $e'), + // backgroundColor: Colors.red, + // ), + // ); + // } \ No newline at end of file diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 8323e4b..50ea38f 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -41,6 +41,8 @@ import 'package:apskel_owner_flutter/application/outlet/current_outlet_loader/cu as _i337; import 'package:apskel_owner_flutter/application/product/product_loader/product_loader_bloc.dart' as _i458; +import 'package:apskel_owner_flutter/application/report/inventory_report/inventory_report_bloc.dart' + as _i346; import 'package:apskel_owner_flutter/application/user/change_password_form/change_password_form_bloc.dart' as _i1030; import 'package:apskel_owner_flutter/application/user/user_edit_form/user_edit_form_bloc.dart' @@ -263,6 +265,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i1030.ChangePasswordFormBloc>( () => _i1030.ChangePasswordFormBloc(gh<_i635.IUserRepository>()), ); + gh.factory<_i346.InventoryReportBloc>( + () => _i346.InventoryReportBloc( + gh<_i477.IAnalyticRepository>(), + gh<_i197.IOutletRepository>(), + ), + ); return this; } } diff --git a/lib/presentation/components/report/inventory_report.dart b/lib/presentation/components/report/inventory_report.dart new file mode 100644 index 0000000..3ad5771 --- /dev/null +++ b/lib/presentation/components/report/inventory_report.dart @@ -0,0 +1,547 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; + +import '../../../common/utils/pdf_service.dart'; +import '../../../domain/analytic/analytic.dart'; +import '../../../domain/outlet/outlet.dart'; + +class InventoryReport { + static final primaryColor = PdfColor.fromHex("36175e"); + + static Future previewPdf({ + required String searchDateFormatted, + required InventoryAnalytic inventory, + required Outlet outlet, + }) async { + final pdf = pw.Document(); + final ByteData dataImage = await rootBundle.load('assets/images/logo.png'); + final Uint8List bytes = dataImage.buffer.asUint8List(); + + final image = pw.MemoryImage(bytes); + pdf.addPage( + pw.MultiPage( + pageFormat: PdfPageFormat.a4, + margin: pw.EdgeInsets.zero, + build: (pw.Context context) { + return [ + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Bagian kiri - Logo dan Info Perusahaan + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // Icon/Logo placeholder (bisa diganti dengan gambar logo) + pw.Container( + width: 40, + height: 40, + child: pw.Image(image), + ), + pw.SizedBox(width: 15), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Apskel', + style: pw.TextStyle( + fontSize: 28, + fontWeight: pw.FontWeight.bold, + color: primaryColor, + ), + ), + pw.SizedBox(height: 4), + pw.Text( + outlet.name, + style: pw.TextStyle( + fontSize: 16, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 2), + pw.Text( + outlet.address, + style: pw.TextStyle( + fontSize: 12, + color: PdfColors.grey600, + ), + ), + ], + ), + ], + ), + // Bagian kanan - Info Laporan + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Text( + 'Laporan Transaksi', + style: pw.TextStyle( + fontSize: 24, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey800, + ), + ), + pw.SizedBox(height: 8), + pw.Text( + searchDateFormatted, + style: pw.TextStyle( + fontSize: 14, + color: PdfColors.grey600, + ), + ), + pw.SizedBox(height: 4), + pw.Text( + 'Laporan', + style: pw.TextStyle( + fontSize: 12, + color: PdfColors.grey500, + ), + ), + ], + ), + ], + ), + ), + pw.Container( + width: double.infinity, + height: 3, + color: primaryColor, + ), + + // Summary + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('1. Ringkasan'), + pw.SizedBox(height: 30), + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Expanded( + flex: 1, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSummaryItem( + 'Total Item', + (inventory.summary.totalProducts).toString(), + ), + _buildSummaryItem( + 'Total Item Masuk', + (inventory.products.fold( + 0, + (sum, item) => sum + (item.totalIn), + )).toString(), + ), + _buildSummaryItem( + 'Total Item Keluar', + (inventory.products.fold( + 0, + (sum, item) => sum + (item.totalOut), + )).toString(), + ), + ], + ), + ), + pw.SizedBox(width: 20), + pw.Expanded( + flex: 1, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSummaryItem( + 'Total Ingredient', + (inventory.summary.totalIngredients).toString(), + ), + _buildSummaryItem( + 'Total Ingredient Masuk', + (inventory.ingredients.fold( + 0, + (sum, item) => sum + (item.totalIn), + )).toString(), + ), + _buildSummaryItem( + 'Total Ingredient Keluar', + (inventory.ingredients.fold( + 0, + (sum, item) => sum + (item.totalOut), + )).toString(), + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Summary Item + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('2. Item'), + pw.SizedBox(height: 30), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // Stock + 3: pw.FlexColumnWidth(2), // Masuk + 4: pw.FlexColumnWidth(2), // Keluar + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Nama'), + _buildHeaderCell('Kategori'), + _buildHeaderCell('Stock'), + _buildHeaderCell('Masuk'), + _buildHeaderCell('Keluar'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // Stock + 3: pw.FlexColumnWidth(2), // Masuk + 4: pw.FlexColumnWidth(2), // Keluar + }, + children: inventory.products + .map( + (item) => _buildProductDataRow( + item, + inventory.products.indexOf(item) % 2 == 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // Stock + 3: pw.FlexColumnWidth(2), // Masuk + 4: pw.FlexColumnWidth(2), // Keluar + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell(''), + _buildTotalCell( + (inventory.products.fold( + 0, + (sum, item) => sum + (item.quantity), + )).toString(), + ), + _buildTotalCell( + (inventory.products.fold( + 0, + (sum, item) => sum + (item.totalIn), + )).toString(), + ), + _buildTotalCell( + (inventory.products.fold( + 0, + (sum, item) => sum + (item.totalOut), + )).toString(), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Summary Ingredient + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('3. Ingredient'), + pw.SizedBox(height: 30), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Name + 1: pw.FlexColumnWidth(1), // Stock + 2: pw.FlexColumnWidth(2), // Masuk + 3: pw.FlexColumnWidth(2), // Keluar + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Nama'), + _buildHeaderCell('Stock'), + _buildHeaderCell('Masuk'), + _buildHeaderCell('Keluar'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Name + 1: pw.FlexColumnWidth(1), // Stock + 2: pw.FlexColumnWidth(2), // Masuk + 3: pw.FlexColumnWidth(2), // Keluar + }, + children: inventory.ingredients + .map( + (item) => _buildIngredientsDataRow( + item, + inventory.ingredients.indexOf(item) % 2 == 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Name + 1: pw.FlexColumnWidth(1), // Stock + 2: pw.FlexColumnWidth(2), // Masuk + 3: pw.FlexColumnWidth(2), // Keluar + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell( + (inventory.ingredients.fold( + 0, + (sum, item) => sum + (item.quantity), + )).toString(), + ), + _buildTotalCell( + (inventory.ingredients.fold( + 0, + (sum, item) => sum + (item.totalIn), + )).toString(), + ), + _buildTotalCell( + (inventory.ingredients.fold( + 0, + (sum, item) => sum + (item.totalOut), + )).toString(), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ]; + }, + ), + ); + + return HelperPdfService.saveDocument( + name: + 'Apskel POS | Inventory Report | ${DateTime.now().millisecondsSinceEpoch}.pdf', + pdf: pdf, + ); + } + + static pw.Widget _buildSectionWidget(String title) { + return pw.Text( + title, + style: pw.TextStyle( + fontSize: 20, + fontWeight: pw.FontWeight.bold, + color: primaryColor, + ), + ); + } + + static pw.Widget _buildSummaryItem( + String label, + String value, { + pw.TextStyle? valueStyle, + pw.TextStyle? labelStyle, + }) { + return pw.Container( + padding: pw.EdgeInsets.only(bottom: 8), + margin: pw.EdgeInsets.only(bottom: 16), + decoration: pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey300)), + ), + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text(label, style: labelStyle), + pw.Text( + value, + style: valueStyle ?? pw.TextStyle(fontWeight: pw.FontWeight.bold), + ), + ], + ), + ); + } + + static pw.Widget _buildHeaderCell(String text) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: pw.Text( + text, + style: pw.TextStyle( + color: PdfColors.white, + fontWeight: pw.FontWeight.bold, + fontSize: 12, + ), + textAlign: pw.TextAlign.center, + ), + ); + } + + static pw.Widget _buildDataCell( + String text, { + pw.Alignment alignment = pw.Alignment.center, + PdfColor? textColor, + }) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + alignment: alignment, + child: pw.Text( + text, + style: pw.TextStyle( + fontSize: 12, + color: textColor ?? PdfColors.black, + fontWeight: pw.FontWeight.normal, + ), + textAlign: alignment == pw.Alignment.centerLeft + ? pw.TextAlign.left + : pw.TextAlign.center, + ), + ); + } + + static pw.Widget _buildTotalCell(String text) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: pw.Text( + text, + style: pw.TextStyle( + color: PdfColors.white, + fontWeight: pw.FontWeight.bold, + fontSize: 12, + ), + textAlign: pw.TextAlign.center, + ), + ); + } + + static pw.TableRow _buildProductDataRow( + InventoryProduct product, + bool isEven, + ) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: product.isZeroStock + ? PdfColors.red100 + : product.isLowStock + ? PdfColors.yellow100 + : isEven + ? PdfColors.grey50 + : PdfColors.white, + ), + children: [ + _buildDataCell(product.productName, alignment: pw.Alignment.centerLeft), + _buildDataCell( + product.categoryName, + alignment: pw.Alignment.centerLeft, + ), + _buildDataCell(product.quantity.toString()), + _buildDataCell(product.totalIn.toString()), + _buildDataCell(product.totalOut.toString()), + ], + ); + } + + static pw.TableRow _buildIngredientsDataRow( + InventoryIngredient item, + bool isEven, + ) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: item.isZeroStock + ? PdfColors.red100 + : item.isLowStock + ? PdfColors.yellow100 + : isEven + ? PdfColors.grey50 + : PdfColors.white, + ), + children: [ + _buildDataCell(item.ingredientName, alignment: pw.Alignment.centerLeft), + _buildDataCell(item.quantity.toString()), + _buildDataCell(item.totalIn.toString()), + _buildDataCell(item.totalOut.toString()), + ], + ); + } +} diff --git a/lib/presentation/pages/download/download_report_page.dart b/lib/presentation/pages/download/download_report_page.dart index 08c8511..ee111dc 100644 --- a/lib/presentation/pages/download/download_report_page.dart +++ b/lib/presentation/pages/download/download_report_page.dart @@ -1,16 +1,34 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import '../../../application/report/inventory_report/inventory_report_bloc.dart'; +import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; +import '../../../common/utils/pdf_service.dart'; +import '../../../common/utils/permission.dart'; +import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/field/date_range_picker_field.dart'; +import '../../components/report/inventory_report.dart'; +import '../../components/toast/flushbar.dart'; @RoutePage() -class DownloadReportPage extends StatefulWidget { +class DownloadReportPage extends StatefulWidget implements AutoRouteWrapper { const DownloadReportPage({super.key}); @override State createState() => _DownloadReportPageState(); + + @override + Widget wrappedRoute(BuildContext context) => BlocProvider( + create: (context) => + getIt()..add(InventoryReportEvent.fetchedOutlet()), + child: this, + ); } class _DownloadReportPageState extends State @@ -26,10 +44,10 @@ class _DownloadReportPageState extends State DateTime? _transactionEndDate; DateTime? _inventoryStartDate; DateTime? _inventoryEndDate; - DateTime? _salesStartDate; - DateTime? _salesEndDate; - DateTime? _customerStartDate; - DateTime? _customerEndDate; + // DateTime? _salesStartDate; + // DateTime? _salesEndDate; + // DateTime? _customerStartDate; + // DateTime? _customerEndDate; @override void initState() { @@ -76,28 +94,9 @@ class _DownloadReportPageState extends State DateTime? endDate, ) { if (startDate == null || endDate == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select both start and end dates'), - backgroundColor: AppColor.error, - ), - ); + AppFlushbar.showError(context, 'Please select both start and end dates'); return; } - - // Implement download logic here - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Downloading $reportType from ${_formatDate(startDate)} to ${_formatDate(endDate)}', - ), - backgroundColor: AppColor.success, - ), - ); - } - - String _formatDate(DateTime date) { - return '${date.day}/${date.month}/${date.year}'; } @override @@ -139,6 +138,7 @@ class _DownloadReportPageState extends State ], startDate: _transactionStartDate, endDate: _transactionEndDate, + isLoading: false, onDateRangeChanged: (start, end) { setState(() { _transactionStartDate = start; @@ -156,79 +156,115 @@ class _DownloadReportPageState extends State const SizedBox(height: 20), // Inventory Report Card - _ReportOptionCard( - title: 'Inventory Report', - subtitle: - 'Export inventory and stock data with trends', - icon: Icons.inventory_2_outlined, - gradient: const [ - AppColor.secondary, - AppColor.secondaryLight, - ], - startDate: _inventoryStartDate, - endDate: _inventoryEndDate, - onDateRangeChanged: (start, end) { - setState(() { - _inventoryStartDate = start; - _inventoryEndDate = end; - }); + BlocBuilder( + builder: (context, state) { + return _ReportOptionCard( + title: 'Inventory Report', + subtitle: + 'Export inventory and stock data with trends', + icon: Icons.inventory_2_outlined, + gradient: const [ + AppColor.secondary, + AppColor.secondaryLight, + ], + startDate: _inventoryStartDate, + endDate: _inventoryEndDate, + isLoading: state.isFetching, + onDateRangeChanged: (start, end) { + setState(() { + _inventoryStartDate = start; + _inventoryEndDate = end; + }); + if (start != null || end != null) { + context.read().add( + InventoryReportEvent.fetchedInventory( + start!, + end!, + ), + ); + } + }, + onDownload: () async { + try { + final status = await PermessionHelper() + .checkPermission(); + if (status) { + final pdfFile = + await InventoryReport.previewPdf( + searchDateFormatted: + "${_inventoryStartDate?.toServerDate} - ${_inventoryEndDate?.toServerDate}", + inventory: state.inventoryAnalytic, + outlet: state.outlet, + ); + log("pdfFile: $pdfFile"); + await HelperPdfService.openFile(pdfFile); + } else { + AppFlushbar.showError( + context, + 'Storage permission is required to save PDF', + ); + } + } catch (e) { + log("Error generating PDF: $e"); + AppFlushbar.showError( + context, + 'Failed to generate PDF: $e', + ); + } + }, + delay: 400, + ); }, - onDownload: () => _downloadReport( - 'Inventory Report', - _inventoryStartDate, - _inventoryEndDate, - ), - delay: 400, ), const SizedBox(height: 20), // Sales Report Card - _ReportOptionCard( - title: 'Sales Report', - subtitle: 'Export sales performance and revenue data', - icon: Icons.trending_up_outlined, - gradient: const [AppColor.info, Color(0xFF64B5F6)], - startDate: _salesStartDate, - endDate: _salesEndDate, - onDateRangeChanged: (start, end) { - setState(() { - _salesStartDate = start; - _salesEndDate = end; - }); - }, - onDownload: () => _downloadReport( - 'Sales Report', - _salesStartDate, - _salesEndDate, - ), - delay: 600, - ), + // _ReportOptionCard( + // title: 'Sales Report', + // subtitle: 'Export sales performance and revenue data', + // icon: Icons.trending_up_outlined, + // gradient: const [AppColor.info, Color(0xFF64B5F6)], + // startDate: _salesStartDate, + // endDate: _salesEndDate, + // onDateRangeChanged: (start, end) { + // setState(() { + // _salesStartDate = start; + // _salesEndDate = end; + // }); + // }, + // onDownload: () => _downloadReport( + // 'Sales Report', + // _salesStartDate, + // _salesEndDate, + // ), + // delay: 600, + // ), - const SizedBox(height: 20), + // const SizedBox(height: 20), - // Customer Report Card - _ReportOptionCard( - title: 'Customer Report', - subtitle: - 'Export customer data and behavior analytics', - icon: Icons.people_outline, - gradient: const [AppColor.warning, Color(0xFFFFB74D)], - startDate: _customerStartDate, - endDate: _customerEndDate, - onDateRangeChanged: (start, end) { - setState(() { - _customerStartDate = start; - _customerEndDate = end; - }); - }, - onDownload: () => _downloadReport( - 'Customer Report', - _customerStartDate, - _customerEndDate, - ), - delay: 800, - ), + // // Customer Report Card + // _ReportOptionCard( + // title: 'Customer Report', + // subtitle: + // 'Export customer data and behavior analytics', + // icon: Icons.people_outline, + // gradient: const [AppColor.warning, Color(0xFFFFB74D)], + // startDate: _customerStartDate, + // endDate: _customerEndDate, + // onDateRangeChanged: (start, end) { + // setState(() { + // _customerStartDate = start; + // _customerEndDate = end; + // }); + // }, + // onDownload: () => _downloadReport( + // 'Customer Report', + // _customerStartDate, + // _customerEndDate, + // ), + // delay: 800, + // ), ], ), ), @@ -253,6 +289,7 @@ class _ReportOptionCard extends StatefulWidget { final DateTime? endDate; final Function(DateTime? startDate, DateTime? endDate) onDateRangeChanged; final VoidCallback onDownload; + final bool isLoading; final int delay; const _ReportOptionCard({ @@ -265,6 +302,7 @@ class _ReportOptionCard extends StatefulWidget { required this.onDateRangeChanged, required this.onDownload, required this.delay, + required this.isLoading, }); @override @@ -431,23 +469,41 @@ class _ReportOptionCardState extends State<_ReportOptionCard> ), elevation: 0, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.download_rounded, - color: widget.gradient.first, - ), - const SizedBox(width: 8), - Text( - 'Download Report', - style: AppStyle.md.copyWith( - color: widget.gradient.first, - fontWeight: FontWeight.bold, + child: widget.isLoading + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SpinKitCircle( + color: widget.gradient.first, + size: 24, + ), + const SizedBox(width: 8), + Text( + 'Loading', + style: AppStyle.md.copyWith( + color: widget.gradient.first, + fontWeight: FontWeight.bold, + ), + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.download_rounded, + color: widget.gradient.first, + ), + const SizedBox(width: 8), + Text( + 'Download Report', + style: AppStyle.md.copyWith( + color: widget.gradient.first, + fontWeight: FontWeight.bold, + ), + ), + ], ), - ), - ], - ), ), ), ], diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7299b5c..86be7eb 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 786ff5c..c842924 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + open_file_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ca4c2ff..00f5060 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import connectivity_plus import device_info_plus import file_selector_macos +import open_file_mac import package_info_plus import path_provider_foundation import shared_preferences_foundation @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index a1d564d..daee97a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.4" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" bloc: dependency: transitive description: @@ -869,6 +885,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e + url: "https://pub.dev" + source: hosted + version: "3.5.10" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://pub.dev" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://pub.dev" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://pub.dev" + source: hosted + version: "0.0.3" package_config: dependency: transitive description: @@ -957,6 +1037,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.dev" + source: hosted + version: "3.11.3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -1021,6 +1157,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" recase: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 81c3a61..3bf55bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,9 @@ dependencies: syncfusion_flutter_datepicker: ^30.2.5 url_launcher: ^6.3.2 device_info_plus: ^11.5.0 + pdf: ^3.11.3 + open_file: ^3.5.10 + permission_handler: ^12.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 384ea41..dbf8289 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 199b881..c22844a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus file_selector_windows + permission_handler_windows url_launcher_windows )