report inventory

This commit is contained in:
efrilm 2025-11-04 00:21:15 +07:00
parent 2a44ce023e
commit dca0f546f9
20 changed files with 4865 additions and 2 deletions

View File

@ -0,0 +1,47 @@
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';
part 'inventory_analytic_loader_event.dart';
part 'inventory_analytic_loader_state.dart';
part 'inventory_analytic_loader_bloc.freezed.dart';
@injectable
class InventoryAnalyticLoaderBloc
extends Bloc<InventoryAnalyticLoaderEvent, InventoryAnalyticLoaderState> {
final IAnalyticRepository _analyticRepository;
InventoryAnalyticLoaderBloc(this._analyticRepository)
: super(InventoryAnalyticLoaderState.initial()) {
on<InventoryAnalyticLoaderEvent>(_onInventoryAnalyticLoaderEvent);
}
Future<void> _onInventoryAnalyticLoaderEvent(
InventoryAnalyticLoaderEvent event,
Emitter<InventoryAnalyticLoaderState> emit,
) {
return event.map(
fetched: (e) async {
emit(state.copyWith(isFetching: true, failureOption: none()));
final result = await _analyticRepository.getInventory(
dateFrom: e.startDate,
dateTo: e.endDate,
);
emit(
result.fold(
(l) => state.copyWith(isFetching: false, failureOption: some(l)),
(r) => state.copyWith(
isFetching: false,
failureOption: none(),
inventoryAnalytic: r,
),
),
);
},
);
}
}

View File

@ -0,0 +1,480 @@
// 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_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 _$InventoryAnalyticLoaderEvent {
DateTime get startDate => throw _privateConstructorUsedError;
DateTime get endDate => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime startDate, DateTime endDate) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime startDate, DateTime endDate)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime startDate, DateTime endDate)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
/// Create a copy of InventoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$InventoryAnalyticLoaderEventCopyWith<InventoryAnalyticLoaderEvent>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $InventoryAnalyticLoaderEventCopyWith<$Res> {
factory $InventoryAnalyticLoaderEventCopyWith(
InventoryAnalyticLoaderEvent value,
$Res Function(InventoryAnalyticLoaderEvent) then,
) =
_$InventoryAnalyticLoaderEventCopyWithImpl<
$Res,
InventoryAnalyticLoaderEvent
>;
@useResult
$Res call({DateTime startDate, DateTime endDate});
}
/// @nodoc
class _$InventoryAnalyticLoaderEventCopyWithImpl<
$Res,
$Val extends InventoryAnalyticLoaderEvent
>
implements $InventoryAnalyticLoaderEventCopyWith<$Res> {
_$InventoryAnalyticLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of InventoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? startDate = null, Object? endDate = null}) {
return _then(
_value.copyWith(
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res>
implements $InventoryAnalyticLoaderEventCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({DateTime startDate, DateTime endDate});
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$InventoryAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of InventoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? startDate = null, Object? endDate = null}) {
return _then(
_$FetchedImpl(
startDate: null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
endDate: null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl({required this.startDate, required this.endDate});
@override
final DateTime startDate;
@override
final DateTime endDate;
@override
String toString() {
return 'InventoryAnalyticLoaderEvent.fetched(startDate: $startDate, endDate: $endDate)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FetchedImpl &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate));
}
@override
int get hashCode => Object.hash(runtimeType, startDate, endDate);
/// Create a copy of InventoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
__$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(DateTime startDate, DateTime endDate) fetched,
}) {
return fetched(startDate, endDate);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(DateTime startDate, DateTime endDate)? fetched,
}) {
return fetched?.call(startDate, endDate);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(DateTime startDate, DateTime endDate)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(startDate, endDate);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements InventoryAnalyticLoaderEvent {
const factory _Fetched({
required final DateTime startDate,
required final DateTime endDate,
}) = _$FetchedImpl;
@override
DateTime get startDate;
@override
DateTime get endDate;
/// Create a copy of InventoryAnalyticLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$InventoryAnalyticLoaderState {
InventoryAnalytic get inventoryAnalytic => throw _privateConstructorUsedError;
Option<AnalyticFailure> get failureOption =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
/// Create a copy of InventoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$InventoryAnalyticLoaderStateCopyWith<InventoryAnalyticLoaderState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $InventoryAnalyticLoaderStateCopyWith<$Res> {
factory $InventoryAnalyticLoaderStateCopyWith(
InventoryAnalyticLoaderState value,
$Res Function(InventoryAnalyticLoaderState) then,
) =
_$InventoryAnalyticLoaderStateCopyWithImpl<
$Res,
InventoryAnalyticLoaderState
>;
@useResult
$Res call({
InventoryAnalytic inventoryAnalytic,
Option<AnalyticFailure> failureOption,
bool isFetching,
});
$InventoryAnalyticCopyWith<$Res> get inventoryAnalytic;
}
/// @nodoc
class _$InventoryAnalyticLoaderStateCopyWithImpl<
$Res,
$Val extends InventoryAnalyticLoaderState
>
implements $InventoryAnalyticLoaderStateCopyWith<$Res> {
_$InventoryAnalyticLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of InventoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? inventoryAnalytic = null,
Object? failureOption = null,
Object? isFetching = null,
}) {
return _then(
_value.copyWith(
inventoryAnalytic: null == inventoryAnalytic
? _value.inventoryAnalytic
: inventoryAnalytic // ignore: cast_nullable_to_non_nullable
as InventoryAnalytic,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
/// Create a copy of InventoryAnalyticLoaderState
/// 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);
});
}
}
/// @nodoc
abstract class _$$InventoryAnalyticLoaderStateImplCopyWith<$Res>
implements $InventoryAnalyticLoaderStateCopyWith<$Res> {
factory _$$InventoryAnalyticLoaderStateImplCopyWith(
_$InventoryAnalyticLoaderStateImpl value,
$Res Function(_$InventoryAnalyticLoaderStateImpl) then,
) = __$$InventoryAnalyticLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
InventoryAnalytic inventoryAnalytic,
Option<AnalyticFailure> failureOption,
bool isFetching,
});
@override
$InventoryAnalyticCopyWith<$Res> get inventoryAnalytic;
}
/// @nodoc
class __$$InventoryAnalyticLoaderStateImplCopyWithImpl<$Res>
extends
_$InventoryAnalyticLoaderStateCopyWithImpl<
$Res,
_$InventoryAnalyticLoaderStateImpl
>
implements _$$InventoryAnalyticLoaderStateImplCopyWith<$Res> {
__$$InventoryAnalyticLoaderStateImplCopyWithImpl(
_$InventoryAnalyticLoaderStateImpl _value,
$Res Function(_$InventoryAnalyticLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of InventoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? inventoryAnalytic = null,
Object? failureOption = null,
Object? isFetching = null,
}) {
return _then(
_$InventoryAnalyticLoaderStateImpl(
inventoryAnalytic: null == inventoryAnalytic
? _value.inventoryAnalytic
: inventoryAnalytic // ignore: cast_nullable_to_non_nullable
as InventoryAnalytic,
failureOption: null == failureOption
? _value.failureOption
: failureOption // ignore: cast_nullable_to_non_nullable
as Option<AnalyticFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$InventoryAnalyticLoaderStateImpl
implements _InventoryAnalyticLoaderState {
_$InventoryAnalyticLoaderStateImpl({
required this.inventoryAnalytic,
required this.failureOption,
this.isFetching = false,
});
@override
final InventoryAnalytic inventoryAnalytic;
@override
final Option<AnalyticFailure> failureOption;
@override
@JsonKey()
final bool isFetching;
@override
String toString() {
return 'InventoryAnalyticLoaderState(inventoryAnalytic: $inventoryAnalytic, failureOption: $failureOption, isFetching: $isFetching)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$InventoryAnalyticLoaderStateImpl &&
(identical(other.inventoryAnalytic, inventoryAnalytic) ||
other.inventoryAnalytic == inventoryAnalytic) &&
(identical(other.failureOption, failureOption) ||
other.failureOption == failureOption) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching));
}
@override
int get hashCode =>
Object.hash(runtimeType, inventoryAnalytic, failureOption, isFetching);
/// Create a copy of InventoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$InventoryAnalyticLoaderStateImplCopyWith<
_$InventoryAnalyticLoaderStateImpl
>
get copyWith =>
__$$InventoryAnalyticLoaderStateImplCopyWithImpl<
_$InventoryAnalyticLoaderStateImpl
>(this, _$identity);
}
abstract class _InventoryAnalyticLoaderState
implements InventoryAnalyticLoaderState {
factory _InventoryAnalyticLoaderState({
required final InventoryAnalytic inventoryAnalytic,
required final Option<AnalyticFailure> failureOption,
final bool isFetching,
}) = _$InventoryAnalyticLoaderStateImpl;
@override
InventoryAnalytic get inventoryAnalytic;
@override
Option<AnalyticFailure> get failureOption;
@override
bool get isFetching;
/// Create a copy of InventoryAnalyticLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$InventoryAnalyticLoaderStateImplCopyWith<
_$InventoryAnalyticLoaderStateImpl
>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,9 @@
part of 'inventory_analytic_loader_bloc.dart';
@freezed
class InventoryAnalyticLoaderEvent with _$InventoryAnalyticLoaderEvent {
const factory InventoryAnalyticLoaderEvent.fetched({
required DateTime startDate,
required DateTime endDate,
}) = _Fetched;
}

View File

@ -0,0 +1,16 @@
part of 'inventory_analytic_loader_bloc.dart';
@freezed
class InventoryAnalyticLoaderState with _$InventoryAnalyticLoaderState {
factory InventoryAnalyticLoaderState({
required InventoryAnalytic inventoryAnalytic,
required Option<AnalyticFailure> failureOption,
@Default(false) bool isFetching,
}) = _InventoryAnalyticLoaderState;
factory InventoryAnalyticLoaderState.initial() =>
InventoryAnalyticLoaderState(
inventoryAnalytic: InventoryAnalytic.empty(),
failureOption: none(),
);
}

View File

@ -15,4 +15,5 @@ class ApiPath {
'/api/v1/analytics/payment-methods'; '/api/v1/analytics/payment-methods';
static const String analyticProfitLoss = '/api/v1/analytics/profit-loss'; static const String analyticProfitLoss = '/api/v1/analytics/profit-loss';
static const String analyticCategories = '/api/v1/analytics/categories'; static const String analyticCategories = '/api/v1/analytics/categories';
static const String analyticInventory = '/api/v1/inventory/report/details';
} }

View File

@ -11,5 +11,6 @@ part 'entities/product_analytic_entity.dart';
part 'entities/payment_method_analytic_entity.dart'; part 'entities/payment_method_analytic_entity.dart';
part 'entities/profit_loss_analytic_entity.dart'; part 'entities/profit_loss_analytic_entity.dart';
part 'entities/category_analytic_entity.dart'; part 'entities/category_analytic_entity.dart';
part 'entities/inventory_analytic_entity.dart';
part 'failures/analytic_failure.dart'; part 'failures/analytic_failure.dart';
part 'repositories/i_analytic_repository.dart'; part 'repositories/i_analytic_repository.dart';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,120 @@
part of '../analytic.dart';
@freezed
class InventoryAnalytic with _$InventoryAnalytic {
const factory InventoryAnalytic({
required InventoryAnalyticSummary summary,
required List<InventoryAnalyticProductItem> products,
required List<InventoryAnalyticIngredientItem> ingredients,
}) = _InventoryAnalytic;
factory InventoryAnalytic.empty() => InventoryAnalytic(
summary: InventoryAnalyticSummary.empty(),
products: const [],
ingredients: const [],
);
}
@freezed
class InventoryAnalyticSummary with _$InventoryAnalyticSummary {
const factory InventoryAnalyticSummary({
required int totalProducts,
required int totalIngredients,
required int totalValue,
required int lowStockProducts,
required int lowStockIngredients,
required int zeroStockProducts,
required int zeroStockIngredients,
required int totalSoldProducts,
required int totalSoldIngredients,
required String outletId,
required String outletName,
required DateTime generatedAt,
}) = _InventoryAnalyticSummary;
factory InventoryAnalyticSummary.empty() => InventoryAnalyticSummary(
totalProducts: 0,
totalIngredients: 0,
totalValue: 0,
lowStockProducts: 0,
lowStockIngredients: 0,
zeroStockProducts: 0,
zeroStockIngredients: 0,
totalSoldProducts: 0,
totalSoldIngredients: 0,
outletId: '',
outletName: '',
generatedAt: DateTime.now(),
);
}
@freezed
class InventoryAnalyticProductItem with _$InventoryAnalyticProductItem {
const factory InventoryAnalyticProductItem({
required String id,
required String productId,
required String productName,
required String categoryName,
required int quantity,
required int reorderLevel,
required num unitCost,
required int totalValue,
required int totalIn,
required int totalOut,
required bool isLowStock,
required bool isZeroStock,
required DateTime updatedAt,
}) = _InventoryAnalyticProductItem;
factory InventoryAnalyticProductItem.empty() => InventoryAnalyticProductItem(
id: '',
productId: '',
productName: '',
categoryName: '',
quantity: 0,
reorderLevel: 0,
unitCost: 0,
totalValue: 0,
totalIn: 0,
totalOut: 0,
isLowStock: false,
isZeroStock: false,
updatedAt: DateTime.now(),
);
}
@freezed
class InventoryAnalyticIngredientItem with _$InventoryAnalyticIngredientItem {
const factory InventoryAnalyticIngredientItem({
required String id,
required String ingredientId,
required String ingredientName,
required String unitName,
required int quantity,
required int reorderLevel,
required num unitCost,
required int totalValue,
required int totalIn,
required int totalOut,
required bool isLowStock,
required bool isZeroStock,
required DateTime updatedAt,
}) = _InventoryAnalyticIngredientItem;
factory InventoryAnalyticIngredientItem.empty() =>
InventoryAnalyticIngredientItem(
id: '',
ingredientId: '',
ingredientName: '',
unitName: '',
quantity: 0,
reorderLevel: 0,
unitCost: 0,
totalValue: 0,
totalIn: 0,
totalOut: 0,
isLowStock: false,
isZeroStock: false,
updatedAt: DateTime.now(),
);
}

View File

@ -25,4 +25,8 @@ abstract class IAnalyticRepository {
required DateTime dateFrom, required DateTime dateFrom,
required DateTime dateTo, required DateTime dateTo,
}); });
Future<Either<AnalyticFailure, InventoryAnalytic>> getInventory({
required DateTime dateFrom,
required DateTime dateTo,
});
} }

View File

@ -11,3 +11,4 @@ part 'dtos/product_analytic_dto.dart';
part 'dtos/payment_method_analytic_dto.dart'; part 'dtos/payment_method_analytic_dto.dart';
part 'dtos/profit_loss_analytic_dto.dart'; part 'dtos/profit_loss_analytic_dto.dart';
part 'dtos/category_analytic_dto.dart'; part 'dtos/category_analytic_dto.dart';
part 'dtos/inventory_analytic_dto.dart';

File diff suppressed because it is too large Load Diff

View File

@ -527,3 +527,146 @@ Map<String, dynamic> _$$CategoryAnalyticItemDtoImplToJson(
'product_count': instance.productCount, 'product_count': instance.productCount,
'order_count': instance.orderCount, 'order_count': instance.orderCount,
}; };
_$InventoryAnalyticDtoImpl _$$InventoryAnalyticDtoImplFromJson(
Map<String, dynamic> json,
) => _$InventoryAnalyticDtoImpl(
summary: json['summary'] == null
? null
: InventoryAnalyticSummaryDto.fromJson(
json['summary'] as Map<String, dynamic>,
),
products: (json['products'] as List<dynamic>?)
?.map(
(e) =>
InventoryAnalyticProductItemDto.fromJson(e as Map<String, dynamic>),
)
.toList(),
ingredients: (json['ingredients'] as List<dynamic>?)
?.map(
(e) => InventoryAnalyticIngredientItemDto.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
Map<String, dynamic> _$$InventoryAnalyticDtoImplToJson(
_$InventoryAnalyticDtoImpl instance,
) => <String, dynamic>{
'summary': instance.summary,
'products': instance.products,
'ingredients': instance.ingredients,
};
_$InventoryAnalyticSummaryDtoImpl _$$InventoryAnalyticSummaryDtoImplFromJson(
Map<String, dynamic> json,
) => _$InventoryAnalyticSummaryDtoImpl(
totalProducts: (json['total_products'] as num?)?.toInt(),
totalIngredients: (json['total_ingredients'] as num?)?.toInt(),
totalValue: (json['total_value'] as num?)?.toInt(),
lowStockProducts: (json['low_stock_products'] as num?)?.toInt(),
lowStockIngredients: (json['low_stock_ingredients'] as num?)?.toInt(),
zeroStockProducts: (json['zero_stock_products'] as num?)?.toInt(),
zeroStockIngredients: (json['zero_stock_ingredients'] as num?)?.toInt(),
totalSoldProducts: (json['total_sold_products'] as num?)?.toInt(),
totalSoldIngredients: (json['total_sold_ingredients'] as num?)?.toInt(),
outletId: json['outlet_id'] as String?,
outletName: json['outlet_name'] as String?,
generatedAt: json['generated_at'] == null
? null
: DateTime.parse(json['generated_at'] as String),
);
Map<String, dynamic> _$$InventoryAnalyticSummaryDtoImplToJson(
_$InventoryAnalyticSummaryDtoImpl instance,
) => <String, dynamic>{
'total_products': instance.totalProducts,
'total_ingredients': instance.totalIngredients,
'total_value': instance.totalValue,
'low_stock_products': instance.lowStockProducts,
'low_stock_ingredients': instance.lowStockIngredients,
'zero_stock_products': instance.zeroStockProducts,
'zero_stock_ingredients': instance.zeroStockIngredients,
'total_sold_products': instance.totalSoldProducts,
'total_sold_ingredients': instance.totalSoldIngredients,
'outlet_id': instance.outletId,
'outlet_name': instance.outletName,
'generated_at': instance.generatedAt?.toIso8601String(),
};
_$InventoryAnalyticProductItemDtoImpl
_$$InventoryAnalyticProductItemDtoImplFromJson(Map<String, dynamic> json) =>
_$InventoryAnalyticProductItemDtoImpl(
id: json['id'] as String?,
productId: json['product_id'] as String?,
productName: json['product_name'] as String?,
categoryName: json['category_name'] as String?,
quantity: (json['quantity'] as num?)?.toInt(),
reorderLevel: (json['reorder_level'] as num?)?.toInt(),
unitCost: json['unit_cost'] as num?,
totalValue: (json['total_value'] as num?)?.toInt(),
totalIn: (json['total_in'] as num?)?.toInt(),
totalOut: (json['total_out'] as num?)?.toInt(),
isLowStock: json['is_low_stock'] as bool?,
isZeroStock: json['is_zero_stock'] as bool?,
updatedAt: json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
);
Map<String, dynamic> _$$InventoryAnalyticProductItemDtoImplToJson(
_$InventoryAnalyticProductItemDtoImpl instance,
) => <String, dynamic>{
'id': instance.id,
'product_id': instance.productId,
'product_name': instance.productName,
'category_name': instance.categoryName,
'quantity': instance.quantity,
'reorder_level': instance.reorderLevel,
'unit_cost': instance.unitCost,
'total_value': instance.totalValue,
'total_in': instance.totalIn,
'total_out': instance.totalOut,
'is_low_stock': instance.isLowStock,
'is_zero_stock': instance.isZeroStock,
'updated_at': instance.updatedAt?.toIso8601String(),
};
_$InventoryAnalyticIngredientItemDtoImpl
_$$InventoryAnalyticIngredientItemDtoImplFromJson(Map<String, dynamic> json) =>
_$InventoryAnalyticIngredientItemDtoImpl(
id: json['id'] as String?,
ingredientId: json['ingredient_id'] as String?,
ingredientName: json['ingredient_name'] as String?,
unitName: json['unit_name'] as String?,
quantity: (json['quantity'] as num?)?.toInt(),
reorderLevel: (json['reorder_level'] as num?)?.toInt(),
unitCost: json['unit_cost'] as num?,
totalValue: (json['total_value'] as num?)?.toInt(),
totalIn: (json['total_in'] as num?)?.toInt(),
totalOut: (json['total_out'] as num?)?.toInt(),
isLowStock: json['is_low_stock'] as bool?,
isZeroStock: json['is_zero_stock'] as bool?,
updatedAt: json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
);
Map<String, dynamic> _$$InventoryAnalyticIngredientItemDtoImplToJson(
_$InventoryAnalyticIngredientItemDtoImpl instance,
) => <String, dynamic>{
'id': instance.id,
'ingredient_id': instance.ingredientId,
'ingredient_name': instance.ingredientName,
'unit_name': instance.unitName,
'quantity': instance.quantity,
'reorder_level': instance.reorderLevel,
'unit_cost': instance.unitCost,
'total_value': instance.totalValue,
'total_in': instance.totalIn,
'total_out': instance.totalOut,
'is_low_stock': instance.isLowStock,
'is_zero_stock': instance.isZeroStock,
'updated_at': instance.updatedAt?.toIso8601String(),
};

View File

@ -9,6 +9,8 @@ import '../../../common/extension/extension.dart';
import '../../../common/function/app_function.dart'; import '../../../common/function/app_function.dart';
import '../../../common/url/api_path.dart'; import '../../../common/url/api_path.dart';
import '../../../domain/analytic/analytic.dart'; import '../../../domain/analytic/analytic.dart';
import '../../../injection.dart';
import '../../outlet/datasources/local_data_provider.dart';
import '../analytic_dtos.dart'; import '../analytic_dtos.dart';
@injectable @injectable
@ -192,4 +194,34 @@ class AnalyticRemoteDataProvider {
return DC.error(AnalyticFailure.serverError(e)); return DC.error(AnalyticFailure.serverError(e));
} }
} }
Future<DC<AnalyticFailure, InventoryAnalyticDto>> fetchInventory({
required DateTime dateFrom,
required DateTime dateTo,
}) async {
try {
final user = await getIt<OutletLocalDatasource>().currentOutlet();
final response = await _apiClient.get(
'${ApiPath.analyticInventory}/${user.id}',
params: {
'date_from': dateFrom.toServerDate(),
'date_to': dateTo.toServerDate(),
},
headers: getAuthorizationHeader(),
);
if (response.data['success'] == false) {
return DC.error(AnalyticFailure.unexpectedError());
}
final inventory = InventoryAnalyticDto.fromJson(
response.data['data'] as Map<String, dynamic>,
);
return DC.data(inventory);
} on ApiFailure catch (e, s) {
log('fetchInventory', name: _logName, error: e, stackTrace: s);
return DC.error(AnalyticFailure.serverError(e));
}
}
} }

View File

@ -0,0 +1,146 @@
part of '../analytic_dtos.dart';
@freezed
class InventoryAnalyticDto with _$InventoryAnalyticDto {
const InventoryAnalyticDto._();
const factory InventoryAnalyticDto({
@JsonKey(name: "summary") InventoryAnalyticSummaryDto? summary,
@JsonKey(name: "products") List<InventoryAnalyticProductItemDto>? products,
@JsonKey(name: "ingredients")
List<InventoryAnalyticIngredientItemDto>? ingredients,
}) = _InventoryAnalyticDto;
factory InventoryAnalyticDto.fromJson(Map<String, dynamic> json) =>
_$InventoryAnalyticDtoFromJson(json);
// Mapper ke domain (opsional)
InventoryAnalytic toDomain() => InventoryAnalytic(
summary: summary?.toDomain() ?? InventoryAnalyticSummary.empty(),
products: products?.map((e) => e.toDomain()).toList() ?? [],
ingredients: ingredients?.map((e) => e.toDomain()).toList() ?? [],
);
}
@freezed
class InventoryAnalyticSummaryDto with _$InventoryAnalyticSummaryDto {
const InventoryAnalyticSummaryDto._();
const factory InventoryAnalyticSummaryDto({
@JsonKey(name: "total_products") int? totalProducts,
@JsonKey(name: "total_ingredients") int? totalIngredients,
@JsonKey(name: "total_value") int? totalValue,
@JsonKey(name: "low_stock_products") int? lowStockProducts,
@JsonKey(name: "low_stock_ingredients") int? lowStockIngredients,
@JsonKey(name: "zero_stock_products") int? zeroStockProducts,
@JsonKey(name: "zero_stock_ingredients") int? zeroStockIngredients,
@JsonKey(name: "total_sold_products") int? totalSoldProducts,
@JsonKey(name: "total_sold_ingredients") int? totalSoldIngredients,
@JsonKey(name: "outlet_id") String? outletId,
@JsonKey(name: "outlet_name") String? outletName,
@JsonKey(name: "generated_at") DateTime? generatedAt,
}) = _InventoryAnalyticSummaryDto;
factory InventoryAnalyticSummaryDto.fromJson(Map<String, dynamic> json) =>
_$InventoryAnalyticSummaryDtoFromJson(json);
// Optional mapper ke entity
InventoryAnalyticSummary toDomain() => InventoryAnalyticSummary(
totalProducts: totalProducts ?? 0,
totalIngredients: totalIngredients ?? 0,
totalValue: totalValue ?? 0,
lowStockProducts: lowStockProducts ?? 0,
lowStockIngredients: lowStockIngredients ?? 0,
zeroStockProducts: zeroStockProducts ?? 0,
zeroStockIngredients: zeroStockIngredients ?? 0,
totalSoldProducts: totalSoldProducts ?? 0,
totalSoldIngredients: totalSoldIngredients ?? 0,
outletId: outletId ?? '',
outletName: outletName ?? '',
generatedAt: generatedAt ?? DateTime.now(),
);
}
@freezed
class InventoryAnalyticProductItemDto with _$InventoryAnalyticProductItemDto {
const InventoryAnalyticProductItemDto._();
const factory InventoryAnalyticProductItemDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "product_id") String? productId,
@JsonKey(name: "product_name") String? productName,
@JsonKey(name: "category_name") String? categoryName,
@JsonKey(name: "quantity") int? quantity,
@JsonKey(name: "reorder_level") int? reorderLevel,
@JsonKey(name: "unit_cost") num? unitCost,
@JsonKey(name: "total_value") int? totalValue,
@JsonKey(name: "total_in") int? totalIn,
@JsonKey(name: "total_out") int? totalOut,
@JsonKey(name: "is_low_stock") bool? isLowStock,
@JsonKey(name: "is_zero_stock") bool? isZeroStock,
@JsonKey(name: "updated_at") DateTime? updatedAt,
}) = _InventoryAnalyticProductItemDto;
factory InventoryAnalyticProductItemDto.fromJson(Map<String, dynamic> json) =>
_$InventoryAnalyticProductItemDtoFromJson(json);
// Mapper ke domain (opsional)
InventoryAnalyticProductItem toDomain() => InventoryAnalyticProductItem(
id: id ?? '',
productId: productId ?? '',
productName: productName ?? '',
categoryName: categoryName ?? '',
quantity: quantity ?? 0,
reorderLevel: reorderLevel ?? 0,
unitCost: unitCost ?? 0,
totalValue: totalValue ?? 0,
totalIn: totalIn ?? 0,
totalOut: totalOut ?? 0,
isLowStock: isLowStock ?? false,
isZeroStock: isZeroStock ?? false,
updatedAt: updatedAt ?? DateTime.now(),
);
}
@freezed
class InventoryAnalyticIngredientItemDto
with _$InventoryAnalyticIngredientItemDto {
const InventoryAnalyticIngredientItemDto._();
const factory InventoryAnalyticIngredientItemDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "ingredient_id") String? ingredientId,
@JsonKey(name: "ingredient_name") String? ingredientName,
@JsonKey(name: "unit_name") String? unitName,
@JsonKey(name: "quantity") int? quantity,
@JsonKey(name: "reorder_level") int? reorderLevel,
@JsonKey(name: "unit_cost") num? unitCost,
@JsonKey(name: "total_value") int? totalValue,
@JsonKey(name: "total_in") int? totalIn,
@JsonKey(name: "total_out") int? totalOut,
@JsonKey(name: "is_low_stock") bool? isLowStock,
@JsonKey(name: "is_zero_stock") bool? isZeroStock,
@JsonKey(name: "updated_at") DateTime? updatedAt,
}) = _InventoryAnalyticIngredientItemDto;
factory InventoryAnalyticIngredientItemDto.fromJson(
Map<String, dynamic> json,
) => _$InventoryAnalyticIngredientItemDtoFromJson(json);
// Mapper ke domain (opsional)
InventoryAnalyticIngredientItem toDomain() => InventoryAnalyticIngredientItem(
id: id ?? '',
ingredientId: ingredientId ?? '',
ingredientName: ingredientName ?? '',
unitName: unitName ?? '',
quantity: quantity ?? 0,
reorderLevel: reorderLevel ?? 0,
unitCost: unitCost ?? 0,
totalValue: totalValue ?? 0,
totalIn: totalIn ?? 0,
totalOut: totalOut ?? 0,
isLowStock: isLowStock ?? false,
isZeroStock: isZeroStock ?? false,
updatedAt: updatedAt ?? DateTime.now(),
);
}

View File

@ -157,4 +157,28 @@ class AnalyticRepository implements IAnalyticRepository {
return left(const AnalyticFailure.unexpectedError()); return left(const AnalyticFailure.unexpectedError());
} }
} }
@override
Future<Either<AnalyticFailure, InventoryAnalytic>> getInventory({
required DateTime dateFrom,
required DateTime dateTo,
}) async {
try {
final result = await _dataProvider.fetchInventory(
dateFrom: dateFrom,
dateTo: dateTo,
);
if (result.hasError) {
return left(result.error!);
}
final inventory = result.data!.toDomain();
return right(inventory);
} catch (e) {
log('getInventoryError', name: _logName, error: e);
return left(const AnalyticFailure.unexpectedError());
}
}
} }

View File

@ -13,6 +13,8 @@ import 'package:apskel_pos_flutter_v2/application/analytic/category_analytic_loa
as _i911; as _i911;
import 'package:apskel_pos_flutter_v2/application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart' import 'package:apskel_pos_flutter_v2/application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart'
as _i80; as _i80;
import 'package:apskel_pos_flutter_v2/application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart'
as _i651;
import 'package:apskel_pos_flutter_v2/application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart' import 'package:apskel_pos_flutter_v2/application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart'
as _i733; as _i733;
import 'package:apskel_pos_flutter_v2/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart' import 'package:apskel_pos_flutter_v2/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'
@ -320,6 +322,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i911.CategoryAnalyticLoaderBloc>( gh.factory<_i911.CategoryAnalyticLoaderBloc>(
() => _i911.CategoryAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), () => _i911.CategoryAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
); );
gh.factory<_i651.InventoryAnalyticLoaderBloc>(
() => _i651.InventoryAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
);
return this; return this;
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart'; import '../../../../../application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart';
import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart'; import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
import '../../../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart';
import '../../../../../application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart'; import '../../../../../application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart';
import '../../../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'; import '../../../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart';
import '../../../../../application/analytic/profit_loss_analytic_loader/profit_loss_analytic_loader_bloc.dart'; import '../../../../../application/analytic/profit_loss_analytic_loader/profit_loss_analytic_loader_bloc.dart';
@ -19,6 +20,7 @@ import '../../../../components/spaces/space.dart';
import '../../../../router/app_router.gr.dart'; import '../../../../router/app_router.gr.dart';
import 'sections/report_category_section.dart'; import 'sections/report_category_section.dart';
import 'sections/report_dashboard_section.dart'; import 'sections/report_dashboard_section.dart';
import 'sections/report_inventory_section.dart';
import 'sections/report_payment_method_section.dart'; import 'sections/report_payment_method_section.dart';
import 'sections/report_product_section.dart'; import 'sections/report_product_section.dart';
import 'sections/report_profit_loss_section.dart'; import 'sections/report_profit_loss_section.dart';
@ -185,7 +187,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
); );
}, },
), ),
6 => Text(state.title), 6 =>
BlocBuilder<
InventoryAnalyticLoaderBloc,
InventoryAnalyticLoaderState
>(
builder: (context, inventory) {
return ReportInventorySection(
menu: reportMenus[state.selectedMenu],
state: inventory,
startDate: state.startDate,
endDate: state.endDate,
);
},
),
7 => 7 =>
BlocBuilder< BlocBuilder<
CategoryAnalyticLoaderBloc, CategoryAnalyticLoaderBloc,
@ -255,6 +270,12 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
), ),
); );
case 6: case 6:
return context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(
startDate: state.startDate,
endDate: state.endDate,
),
);
case 7: case 7:
return context.read<CategoryAnalyticLoaderBloc>().add( return context.read<CategoryAnalyticLoaderBloc>().add(
CategoryAnalyticLoaderEvent.fetched( CategoryAnalyticLoaderEvent.fetched(
@ -316,7 +337,6 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
), ),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => getIt<CategoryAnalyticLoaderBloc>() create: (context) => getIt<CategoryAnalyticLoaderBloc>()
..add( ..add(
@ -326,6 +346,15 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
), ),
), ),
), ),
BlocProvider(
create: (context) => getIt<InventoryAnalyticLoaderBloc>()
..add(
InventoryAnalyticLoaderEvent.fetched(
startDate: DateTime.now().subtract(const Duration(days: 30)),
endDate: DateTime.now(),
),
),
),
], ],
child: this, child: this,
); );

View File

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart';
import '../../../../../../common/data/report_menu.dart';
import '../../../../../../common/extension/extension.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../components/error/analytic_error_state_widget.dart';
import '../../../../../components/loader/loader_with_text.dart';
import '../../../../../components/spaces/space.dart';
import '../../../../../components/widgets/report/report_header.dart';
import '../../../../../components/widgets/report/report_summary_card.dart';
import '../widgets/inventory/report_inventory_ingredient_tab.dart';
import '../widgets/inventory/report_inventory_product_tab.dart';
class ReportInventorySection extends StatefulWidget {
final ReportMenu menu;
final InventoryAnalyticLoaderState state;
final DateTime startDate;
final DateTime endDate;
const ReportInventorySection({
super.key,
required this.menu,
required this.state,
required this.startDate,
required this.endDate,
});
@override
State<ReportInventorySection> createState() => _ReportInventorySectionState();
}
class _ReportInventorySectionState extends State<ReportInventorySection> {
int _selectedTabIndex = 0;
@override
Widget build(BuildContext context) {
if (widget.state.isFetching) {
return const Center(child: LoaderWithText());
}
return widget.state.failureOption.fold(
() => RefreshIndicator(
onRefresh: () {
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(
startDate: widget.startDate,
endDate: widget.endDate,
),
);
return Future.value();
},
child: ListView(
padding: EdgeInsets.all(16),
children: [
ReportHeader(
menu: widget.menu,
endDate: widget.endDate,
startDate: widget.startDate,
),
_buildSummary(),
Container(
padding: const EdgeInsets.only(top: 16),
child: Row(
children: [
_buildTab('Produk', 0),
SpaceWidth(12),
_buildTab('Bahan Baku', 1),
],
),
),
SpaceHeight(16),
_selectedTabIndex == 0
? ReportInventoryProductTab(
data: widget.state.inventoryAnalytic,
)
: ReportInventoryIngredientTab(
data: widget.state.inventoryAnalytic,
),
],
),
),
(f) => AnalyticErrorStateWidget(
failure: f,
menu: widget.menu,
onRefresh: () {
context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(
startDate: widget.startDate,
endDate: widget.endDate,
),
);
},
),
);
}
Padding _buildSummary() {
return Padding(
padding: EdgeInsetsGeometry.only(top: 16),
child: Row(
children: [
Expanded(
child: ReportSummaryCard(
color: AppColor.success,
icon: Icons.inventory_2_outlined,
title: 'Total Produk',
value: widget.state.inventoryAnalytic.summary.totalProducts
.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.info,
icon: Icons.list_alt_outlined,
title: 'Total Bahan',
value: widget.state.inventoryAnalytic.summary.totalIngredients
.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.primary,
icon: Icons.monetization_on_outlined,
title: 'Total Nilai',
value: widget
.state
.inventoryAnalytic
.summary
.totalValue
.currencyFormatRpV2,
),
),
],
),
);
}
Widget _buildTab(String title, int index) {
bool isActive = _selectedTabIndex == index;
return GestureDetector(
onTap: () {
setState(() {
_selectedTabIndex = index;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: isActive ? AppColor.primary : AppColor.white,
borderRadius: BorderRadius.circular(25),
border: Border.all(
color: isActive ? AppColor.primary : AppColor.border,
width: 1,
),
),
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isActive ? AppColor.white : AppColor.textSecondary,
),
),
),
);
}
}

View File

@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import '../../../../../../../common/theme/theme.dart';
import '../../../../../../../domain/analytic/analytic.dart';
import '../../../../../../components/spaces/space.dart';
import '../../../../../../components/widgets/report/report_summary_card.dart';
class ReportInventoryIngredientTab extends StatelessWidget {
final InventoryAnalytic data;
const ReportInventoryIngredientTab({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Expanded(
child: ReportSummaryCard(
color: AppColor.success,
icon: Icons.warning_amber_outlined,
title: 'Low Stok',
value: data.summary.lowStockIngredients.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.error,
icon: Icons.block_outlined,
title: 'Zero Stok',
value: data.summary.zeroStockIngredients.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.error,
icon: Icons.shopping_cart_checkout_outlined,
title: 'Total Sold',
value: data.summary.totalSoldIngredients.toString(),
),
),
],
),
Container(
margin: EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: AppColor.primary, // Purple color
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Table(
columnWidths: const {
0: FlexColumnWidth(2.5), // Name
1: FlexColumnWidth(1), // Stock
2: FlexColumnWidth(2), // Masuk
3: FlexColumnWidth(2), // Keluar
},
children: [
TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Stock'),
_buildHeaderCell('Masuk'),
_buildHeaderCell('Keluar'),
],
),
],
),
),
Container(
decoration: BoxDecoration(color: AppColor.white),
child: Table(
columnWidths: {
0: FlexColumnWidth(2.5), // Name
1: FlexColumnWidth(1), // Stock
2: FlexColumnWidth(2), // Masuk
3: FlexColumnWidth(2), // Keluar
},
children: data.ingredients
.map(
(item) => _buildIngredientsDataRow(
item,
data.ingredients.indexOf(item) % 2 == 0,
),
)
.toList(),
),
),
Container(
decoration: BoxDecoration(
color: AppColor.primary, // Purple color
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Table(
columnWidths: const {
0: FlexColumnWidth(2.5), // Name
1: FlexColumnWidth(1), // Stock
2: FlexColumnWidth(2), // Masuk
3: FlexColumnWidth(2), // Keluar
},
children: [
TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
(data.ingredients.fold<num>(
0,
(sum, item) => sum + (item.quantity),
)).toString(),
),
_buildTotalCell(
(data.ingredients.fold<num>(
0,
(sum, item) => sum + (item.totalIn),
)).toString(),
),
_buildTotalCell(
(data.ingredients.fold<num>(
0,
(sum, item) => sum + (item.totalOut),
)).toString(),
),
],
),
],
),
),
],
);
}
TableRow _buildIngredientsDataRow(
InventoryAnalyticIngredientItem item,
bool isEven,
) {
return TableRow(
decoration: BoxDecoration(
color: item.isZeroStock
? Colors.red.shade100
: item.isLowStock
? Colors.yellow.shade100
: isEven
? Colors.grey.shade50
: AppColor.white,
),
children: [
_buildDataCell(item.ingredientName, alignment: Alignment.centerLeft),
_buildDataCell(item.quantity.toString()),
_buildDataCell(item.totalIn.toString()),
_buildDataCell(item.totalOut.toString()),
],
);
}
Widget _buildDataCell(
String text, {
Alignment alignment = Alignment.center,
Color? textColor,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
alignment: alignment,
child: Text(
text,
style: AppStyle.sm.copyWith(
color: textColor ?? AppColor.black,
fontWeight: FontWeight.normal,
),
textAlign: alignment == Alignment.centerLeft
? TextAlign.left
: TextAlign.center,
),
);
}
Widget _buildHeaderCell(String text) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Text(
text,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildTotalCell(String text) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Text(
text,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
}

View File

@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import '../../../../../../../common/theme/theme.dart';
import '../../../../../../../domain/analytic/analytic.dart';
import '../../../../../../components/spaces/space.dart';
import '../../../../../../components/widgets/report/report_summary_card.dart';
class ReportInventoryProductTab extends StatelessWidget {
final InventoryAnalytic data;
const ReportInventoryProductTab({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Expanded(
child: ReportSummaryCard(
color: AppColor.success,
icon: Icons.warning_amber_outlined,
title: 'Low Stok',
value: data.summary.lowStockProducts.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.error,
icon: Icons.block_outlined,
title: 'Zero Stok',
value: data.summary.zeroStockProducts.toString(),
),
),
SpaceWidth(12),
Expanded(
child: ReportSummaryCard(
color: AppColor.error,
icon: Icons.shopping_cart_checkout_outlined,
title: 'Total Sold',
value: data.summary.totalSoldProducts.toString(),
),
),
],
),
Container(
margin: EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: AppColor.primary, // Purple color
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Table(
columnWidths: const {
0: FlexColumnWidth(2.5), // Produk
1: FlexColumnWidth(2), // Kategori
2: FlexColumnWidth(1), // Stock
3: FlexColumnWidth(2), // Masuk
4: FlexColumnWidth(2), // Keluar
},
children: [
TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Kategori'),
_buildHeaderCell('Stock'),
_buildHeaderCell('Masuk'),
_buildHeaderCell('Keluar'),
],
),
],
),
),
Container(
decoration: BoxDecoration(color: AppColor.white),
child: Table(
columnWidths: {
0: FlexColumnWidth(2.5), // Produk
1: FlexColumnWidth(2), // Kategori
2: FlexColumnWidth(1), // Stock
3: FlexColumnWidth(2), // Masuk
4: FlexColumnWidth(2), // Keluar
},
children: data.products
.map(
(item) => _buildProductDataRow(
item,
data.products.indexOf(item) % 2 == 0,
),
)
.toList(),
),
),
Container(
decoration: BoxDecoration(
color: AppColor.primary, // Purple color
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Table(
columnWidths: const {
0: FlexColumnWidth(2.5), // Produk
1: FlexColumnWidth(2), // Kategori
2: FlexColumnWidth(1), // Stock
3: FlexColumnWidth(2), // Masuk
4: FlexColumnWidth(2), // Keluar
},
children: [
TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell(
(data.products.fold<num>(
0,
(sum, item) => sum + (item.quantity),
)).toString(),
),
_buildTotalCell(
(data.products.fold<num>(
0,
(sum, item) => sum + (item.totalIn),
)).toString(),
),
_buildTotalCell(
(data.products.fold<num>(
0,
(sum, item) => sum + (item.totalOut),
)).toString(),
),
],
),
],
),
),
],
);
}
TableRow _buildProductDataRow(
InventoryAnalyticProductItem product,
bool isEven,
) {
return TableRow(
decoration: BoxDecoration(
color: product.isZeroStock
? Colors.red.shade100
: product.isLowStock
? Colors.yellow.shade100
: isEven
? Colors.grey.shade50
: AppColor.white,
),
children: [
_buildDataCell(product.productName, alignment: Alignment.centerLeft),
_buildDataCell(product.categoryName, alignment: Alignment.centerLeft),
_buildDataCell(product.quantity.toString()),
_buildDataCell(product.totalIn.toString()),
_buildDataCell(product.totalOut.toString()),
],
);
}
Widget _buildDataCell(
String text, {
Alignment alignment = Alignment.center,
Color? textColor,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
alignment: alignment,
child: Text(
text,
style: AppStyle.sm.copyWith(
color: textColor ?? AppColor.black,
fontWeight: FontWeight.normal,
),
textAlign: alignment == Alignment.centerLeft
? TextAlign.left
: TextAlign.center,
),
);
}
Widget _buildHeaderCell(String text) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Text(
text,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildTotalCell(String text) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Text(
text,
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
);
}
}