2025-07-30 22:38:44 +07:00
|
|
|
import 'dart:io';
|
|
|
|
|
import 'dart:developer';
|
|
|
|
|
|
|
|
|
|
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
|
|
|
|
|
import 'package:enaklo_pos/core/extensions/int_ext.dart';
|
|
|
|
|
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
|
|
|
|
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
|
|
|
|
|
import 'package:pdf/widgets.dart';
|
|
|
|
|
import 'package:pdf/pdf.dart';
|
|
|
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
|
|
|
|
class RevenueInvoice {
|
|
|
|
|
static late Font ttf;
|
|
|
|
|
static Future<File> generate(
|
|
|
|
|
SummaryModel summaryModel,
|
|
|
|
|
String searchDateFormatted,
|
|
|
|
|
) async {
|
|
|
|
|
try {
|
|
|
|
|
log("Starting PDF generation for summary report");
|
|
|
|
|
log("Summary model: ${summaryModel.toMap()}");
|
|
|
|
|
log("Search date formatted: $searchDateFormatted");
|
2025-08-01 18:27:40 +07:00
|
|
|
|
2025-07-30 22:38:44 +07:00
|
|
|
final pdf = Document();
|
|
|
|
|
log("PDF document created");
|
2025-08-01 18:27:40 +07:00
|
|
|
|
2025-07-30 22:38:44 +07:00
|
|
|
// Load logo image
|
|
|
|
|
log("Loading logo image...");
|
2025-08-01 18:27:40 +07:00
|
|
|
final ByteData dataImage =
|
|
|
|
|
await rootBundle.load('assets/images/logo.png');
|
2025-07-30 22:38:44 +07:00
|
|
|
final Uint8List bytes = dataImage.buffer.asUint8List();
|
|
|
|
|
final image = pw.MemoryImage(bytes);
|
|
|
|
|
log("Logo image loaded successfully, size: ${bytes.length} bytes");
|
|
|
|
|
|
|
|
|
|
log("Adding page to PDF...");
|
|
|
|
|
pdf.addPage(
|
|
|
|
|
MultiPage(
|
|
|
|
|
build: (context) => [
|
|
|
|
|
buildHeader(summaryModel, image, searchDateFormatted),
|
|
|
|
|
SizedBox(height: 1 * PdfPageFormat.cm),
|
|
|
|
|
buildTotal(summaryModel),
|
|
|
|
|
],
|
|
|
|
|
footer: (context) => buildFooter(summaryModel),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
log("PDF page added successfully");
|
|
|
|
|
|
|
|
|
|
log("Saving PDF document...");
|
|
|
|
|
return HelperPdfService.saveDocument(
|
|
|
|
|
name:
|
2025-08-01 18:27:40 +07:00
|
|
|
'Apskel POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
|
2025-07-30 22:38:44 +07:00
|
|
|
pdf: pdf,
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log("Error generating PDF: $e");
|
|
|
|
|
log("Error stack trace: ${StackTrace.current}");
|
|
|
|
|
return Future.error("Failed to generate PDF: $e");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Widget buildHeader(
|
|
|
|
|
SummaryModel invoice,
|
|
|
|
|
MemoryImage image,
|
|
|
|
|
String searchDateFormatted,
|
|
|
|
|
) =>
|
|
|
|
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
|
|
|
|
Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
SizedBox(height: 1 * PdfPageFormat.cm),
|
2025-08-01 18:27:40 +07:00
|
|
|
Text('Apskel POS | Summary Sales Report',
|
2025-07-30 22:38:44 +07:00
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
)),
|
|
|
|
|
SizedBox(height: 0.2 * PdfPageFormat.cm),
|
|
|
|
|
Text(
|
|
|
|
|
"Data: $searchDateFormatted",
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'Created At: ${DateTime.now().toFormattedDate3()}',
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
Image(
|
|
|
|
|
image,
|
|
|
|
|
width: 80.0,
|
|
|
|
|
height: 80.0,
|
|
|
|
|
fit: BoxFit.fill,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
static Widget buildTotal(SummaryModel summaryModel) {
|
|
|
|
|
log("Building total section with summary model: ${summaryModel.toMap()}");
|
2025-08-01 18:27:40 +07:00
|
|
|
|
2025-07-30 22:38:44 +07:00
|
|
|
// Helper function to safely parse string to int
|
|
|
|
|
int safeParseInt(String? value) {
|
|
|
|
|
if (value == null || value.isEmpty) return 0;
|
|
|
|
|
try {
|
|
|
|
|
return int.parse(value.replaceAll('.00', ''));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log("Error parsing value '$value' to int: $e");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-01 18:27:40 +07:00
|
|
|
|
2025-07-30 22:38:44 +07:00
|
|
|
return Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Revenue',
|
|
|
|
|
value: safeParseInt(summaryModel.totalRevenue).currencyFormatRp,
|
|
|
|
|
unite: true,
|
|
|
|
|
),
|
|
|
|
|
Divider(),
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Sub Total',
|
|
|
|
|
titleStyle: TextStyle(fontWeight: FontWeight.normal),
|
|
|
|
|
value: safeParseInt(summaryModel.totalSubtotal).currencyFormatRp,
|
|
|
|
|
unite: true,
|
|
|
|
|
),
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Discount',
|
|
|
|
|
titleStyle: TextStyle(fontWeight: FontWeight.normal),
|
2025-08-01 18:27:40 +07:00
|
|
|
value:
|
|
|
|
|
"- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
|
2025-07-30 22:38:44 +07:00
|
|
|
unite: true,
|
|
|
|
|
textStyle: TextStyle(
|
|
|
|
|
color: PdfColor.fromHex('#FF0000'),
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Tax',
|
|
|
|
|
titleStyle: TextStyle(fontWeight: FontWeight.normal),
|
|
|
|
|
value: "- ${safeParseInt(summaryModel.totalTax).currencyFormatRp}",
|
|
|
|
|
textStyle: TextStyle(
|
|
|
|
|
color: PdfColor.fromHex('#FF0000'),
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
unite: true,
|
|
|
|
|
),
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Service Charge',
|
|
|
|
|
titleStyle: TextStyle(
|
|
|
|
|
fontWeight: FontWeight.normal,
|
|
|
|
|
),
|
2025-08-01 18:27:40 +07:00
|
|
|
value:
|
|
|
|
|
safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
|
2025-07-30 22:38:44 +07:00
|
|
|
unite: true,
|
|
|
|
|
),
|
|
|
|
|
Divider(),
|
|
|
|
|
buildText(
|
|
|
|
|
title: 'Total ',
|
|
|
|
|
titleStyle: TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
value: (summaryModel.total ?? 0).currencyFormatRp,
|
|
|
|
|
unite: true,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 2 * PdfPageFormat.mm),
|
|
|
|
|
Container(height: 1, color: PdfColors.grey400),
|
|
|
|
|
SizedBox(height: 0.5 * PdfPageFormat.mm),
|
|
|
|
|
Container(height: 1, color: PdfColors.grey400),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Widget buildFooter(SummaryModel summaryModel) => Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Divider(),
|
|
|
|
|
SizedBox(height: 2 * PdfPageFormat.mm),
|
|
|
|
|
buildSimpleText(
|
|
|
|
|
title: 'Address',
|
|
|
|
|
value:
|
|
|
|
|
'Jalan Melati No. 12, Mranggen, Demak, Central Java, 89568'),
|
|
|
|
|
SizedBox(height: 1 * PdfPageFormat.mm),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
static buildSimpleText({
|
|
|
|
|
required String title,
|
|
|
|
|
required String value,
|
|
|
|
|
}) {
|
|
|
|
|
final style = TextStyle(fontWeight: FontWeight.bold);
|
|
|
|
|
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Text(title, style: style),
|
|
|
|
|
SizedBox(width: 2 * PdfPageFormat.mm),
|
|
|
|
|
Text(value),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static buildText({
|
|
|
|
|
required String title,
|
|
|
|
|
required String value,
|
|
|
|
|
double width = double.infinity,
|
|
|
|
|
TextStyle? titleStyle,
|
|
|
|
|
TextStyle? textStyle,
|
|
|
|
|
bool unite = false,
|
|
|
|
|
}) {
|
|
|
|
|
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
|
|
|
|
|
final style2 = textStyle ?? TextStyle(fontWeight: FontWeight.bold);
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
width: width,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(child: Text(title, style: style)),
|
|
|
|
|
Text(value, style: style2),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|