548 lines
20 KiB
Dart
548 lines
20 KiB
Dart
|
|
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<File> 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<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.totalIn),
|
||
|
|
)).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).toString(),
|
||
|
|
),
|
||
|
|
_buildSummaryItem(
|
||
|
|
'Total Ingredient Masuk',
|
||
|
|
(inventory.ingredients.fold<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.totalIn),
|
||
|
|
)).toString(),
|
||
|
|
),
|
||
|
|
_buildSummaryItem(
|
||
|
|
'Total Ingredient Keluar',
|
||
|
|
(inventory.ingredients.fold<num>(
|
||
|
|
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<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.quantity),
|
||
|
|
)).toString(),
|
||
|
|
),
|
||
|
|
_buildTotalCell(
|
||
|
|
(inventory.products.fold<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.totalIn),
|
||
|
|
)).toString(),
|
||
|
|
),
|
||
|
|
_buildTotalCell(
|
||
|
|
(inventory.products.fold<num>(
|
||
|
|
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<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.quantity),
|
||
|
|
)).toString(),
|
||
|
|
),
|
||
|
|
_buildTotalCell(
|
||
|
|
(inventory.ingredients.fold<num>(
|
||
|
|
0,
|
||
|
|
(sum, item) => sum + (item.totalIn),
|
||
|
|
)).toString(),
|
||
|
|
),
|
||
|
|
_buildTotalCell(
|
||
|
|
(inventory.ingredients.fold<num>(
|
||
|
|
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()),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|