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
@@ -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/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/product_analytic_loader/product_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 'sections/report_category_section.dart';
import 'sections/report_dashboard_section.dart';
import 'sections/report_inventory_section.dart';
import 'sections/report_payment_method_section.dart';
import 'sections/report_product_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 =>
BlocBuilder<
CategoryAnalyticLoaderBloc,
@@ -255,6 +270,12 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
),
);
case 6:
return context.read<InventoryAnalyticLoaderBloc>().add(
InventoryAnalyticLoaderEvent.fetched(
startDate: state.startDate,
endDate: state.endDate,
),
);
case 7:
return context.read<CategoryAnalyticLoaderBloc>().add(
CategoryAnalyticLoaderEvent.fetched(
@@ -316,7 +337,6 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
),
),
),
BlocProvider(
create: (context) => getIt<CategoryAnalyticLoaderBloc>()
..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,
);
@@ -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,
),
),
),
);
}
}
@@ -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,
),
);
}
}
@@ -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,
),
);
}
}