feat: inventory report pdf
This commit is contained in:
parent
34c0ad5411
commit
2c75fcf582
568
lib/core/utils/inventory_report.dart
Normal file
568
lib/core/utils/inventory_report.dart
Normal file
@ -0,0 +1,568 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
|
||||
import 'package:enaklo_pos/data/datasources/outlet_local_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/inventory_analytic_response_model.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
class InventoryReport {
|
||||
static final primaryColor = PdfColor.fromHex("36175e");
|
||||
|
||||
static Future<File> previewPdf({
|
||||
required String searchDateFormatted,
|
||||
required InventoryAnalyticData? inventory,
|
||||
}) async {
|
||||
final pdf = pw.Document();
|
||||
final ByteData dataImage = await rootBundle.load('assets/logo/logo.png');
|
||||
final Uint8List bytes = dataImage.buffer.asUint8List();
|
||||
final outlet = await OutletLocalDatasource().get();
|
||||
|
||||
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 ?? 0)
|
||||
.toString(),
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Total Item Masuk',
|
||||
(inventory?.products.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalIn)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Total Item Keluar',
|
||||
(inventory?.products.fold<num>(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 ?? 0)
|
||||
.toString(),
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Total Ingredient Masuk',
|
||||
(inventory?.ingredients.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalIn)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Total Ingredient Keluar',
|
||||
(inventory?.ingredients.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalOut)) ??
|
||||
0)
|
||||
.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<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.quantity)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildTotalCell(
|
||||
(inventory?.products.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalIn)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildTotalCell(
|
||||
(inventory?.products.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalOut)) ??
|
||||
0)
|
||||
.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<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.quantity)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildTotalCell(
|
||||
(inventory?.ingredients.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalIn)) ??
|
||||
0)
|
||||
.toString(),
|
||||
),
|
||||
_buildTotalCell(
|
||||
(inventory?.ingredients.fold<num>(
|
||||
0,
|
||||
(sum, item) =>
|
||||
sum + (item.totalOut)) ??
|
||||
0)
|
||||
.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(
|
||||
InventoryProductItem 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(
|
||||
InventoryIngredientItem 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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -187,7 +187,7 @@ class TransactionReport {
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
_buildSummaryItem(
|
||||
'Total Total Terjual',
|
||||
'Total Terjual',
|
||||
(profitLossData?.summary.totalOrders ?? 0)
|
||||
.toString(),
|
||||
),
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:enaklo_pos/core/constants/colors.dart';
|
||||
import 'package:enaklo_pos/core/extensions/string_ext.dart';
|
||||
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
|
||||
import 'package:enaklo_pos/core/utils/inventory_report.dart';
|
||||
import 'package:enaklo_pos/core/utils/permession_handler.dart';
|
||||
import 'package:enaklo_pos/data/models/response/inventory_analytic_response_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -73,7 +78,38 @@ class _InventoryReportWidgetState extends State<InventoryReportWidget> {
|
||||
children: [
|
||||
// Download Button
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
onTap: () async {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user