first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppIconGenerator {
|
||||
static Future<Uint8List> generateAppIcon() async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
const size = Size(1024, 1024);
|
||||
|
||||
// Create a white background
|
||||
final paint = Paint()..color = Colors.white;
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
|
||||
|
||||
// Create a blue circle background
|
||||
paint.color = const Color(0xFF2196F3); // Material Blue
|
||||
canvas.drawCircle(
|
||||
Offset(size.width / 2, size.height / 2),
|
||||
size.width * 0.4,
|
||||
paint,
|
||||
);
|
||||
|
||||
// Draw the gift box icon
|
||||
_drawGiftBox(canvas, size);
|
||||
|
||||
// Draw the text
|
||||
_drawText(canvas, size);
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(1024, 1024);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
static void _drawGiftBox(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = size.width * 0.02;
|
||||
|
||||
final centerX = size.width / 2;
|
||||
final centerY = size.height / 2 - size.height * 0.1;
|
||||
final boxSize = size.width * 0.15;
|
||||
|
||||
// Draw gift box
|
||||
final boxRect = Rect.fromCenter(
|
||||
center: Offset(centerX, centerY),
|
||||
width: boxSize,
|
||||
height: boxSize,
|
||||
);
|
||||
canvas.drawRect(boxRect, paint);
|
||||
|
||||
// Draw bow
|
||||
final bowWidth = boxSize * 0.8;
|
||||
final bowHeight = boxSize * 0.3;
|
||||
final bowRect = Rect.fromCenter(
|
||||
center: Offset(centerX, centerY - boxSize / 2),
|
||||
width: bowWidth,
|
||||
height: bowHeight,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(bowRect, Radius.circular(bowHeight / 2)),
|
||||
paint,
|
||||
);
|
||||
|
||||
// Draw 'e' inside the box
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'e',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: boxSize * 0.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
centerX - textPainter.width / 2,
|
||||
centerY - textPainter.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void _drawText(Canvas canvas, Size size) {
|
||||
// Draw "ENAKLO"
|
||||
final enakloPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'ENAKLO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: size.width * 0.08,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
enakloPainter.layout();
|
||||
enakloPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
size.width / 2 - enakloPainter.width / 2,
|
||||
size.height / 2 + size.height * 0.05,
|
||||
),
|
||||
);
|
||||
|
||||
// Draw "POS"
|
||||
final posPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'POS',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: size.width * 0.06,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
posPainter.layout();
|
||||
posPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
size.width / 2 - posPainter.width / 2,
|
||||
size.height / 2 + size.height * 0.15,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateFormatter {
|
||||
static String formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}-${_addZeroPrefix(dateTime.month)}-${_addZeroPrefix(dateTime.day)}';
|
||||
}
|
||||
|
||||
static String _addZeroPrefix(int value) {
|
||||
return value.toString().padLeft(2, '0');
|
||||
}
|
||||
|
||||
static String formatDateTime2(String dateTimeString) {
|
||||
final dateTime = DateTime.parse(dateTimeString);
|
||||
final formatter = DateFormat(
|
||||
'dd MMMM yyyy, HH:mm',
|
||||
);
|
||||
return formatter.format(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdf/widgets.dart';
|
||||
|
||||
class HelperPdfService {
|
||||
static Future<File> saveDocument({
|
||||
required String name,
|
||||
required Document pdf,
|
||||
}) async {
|
||||
try {
|
||||
log("Starting PDF save process for: $name");
|
||||
log("PDF document object: $pdf");
|
||||
|
||||
final bytes = await pdf.save();
|
||||
log("PDF bytes generated successfully, size: ${bytes.length} bytes");
|
||||
|
||||
if (bytes.isEmpty) {
|
||||
log("WARNING: PDF bytes are empty!");
|
||||
return Future.error("PDF bytes are empty");
|
||||
}
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
log("Documents directory: ${dir.path}");
|
||||
|
||||
final file = File('${dir.path}/$name');
|
||||
log("Saving PDF to: ${file.path}");
|
||||
|
||||
await file.writeAsBytes(bytes);
|
||||
log("PDF saved successfully to: ${file.path}");
|
||||
|
||||
// Verify file was created
|
||||
if (await file.exists()) {
|
||||
final fileSize = await file.length();
|
||||
log("File exists and size is: $fileSize bytes");
|
||||
} else {
|
||||
log("ERROR: File was not created!");
|
||||
return Future.error("File was not created");
|
||||
}
|
||||
|
||||
return file;
|
||||
} catch (e) {
|
||||
log("Failed to save document: $e");
|
||||
log("Error stack trace: ${StackTrace.current}");
|
||||
return Future.error("Failed to save document: $e");
|
||||
}
|
||||
}
|
||||
|
||||
static Future openFile(File file) async {
|
||||
try {
|
||||
final url = file.path;
|
||||
log("Attempting to open file: $url");
|
||||
|
||||
if (!await file.exists()) {
|
||||
log("ERROR: File does not exist: $url");
|
||||
return;
|
||||
}
|
||||
|
||||
final fileSize = await file.length();
|
||||
log("File exists and size is: $fileSize bytes");
|
||||
|
||||
log("Calling OpenFile.open...");
|
||||
final result = await OpenFile.open(url, type: "application/pdf");
|
||||
log("OpenFile result: $result");
|
||||
|
||||
if (result.type == ResultType.done) {
|
||||
log("File opened successfully");
|
||||
} else {
|
||||
log("File opening failed with result: ${result.type}");
|
||||
log("Error message: ${result.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
log("Failed to open file: $e");
|
||||
log("Error stack trace: ${StackTrace.current}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
|
||||
import 'package:enaklo_pos/core/extensions/int_ext.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
|
||||
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart';
|
||||
import 'package:pdf/widgets.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
class ItemSalesInvoice {
|
||||
static late Font ttf;
|
||||
static Future<File> generate(
|
||||
List<ItemSales> itemSales, String searchDateFormatted) async {
|
||||
final pdf = Document();
|
||||
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
|
||||
// ttf = Font.ttf(data);
|
||||
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
|
||||
final Uint8List bytes = dataImage.buffer.asUint8List();
|
||||
|
||||
// Membuat objek Image dari gambar
|
||||
final image = pw.MemoryImage(bytes);
|
||||
|
||||
pdf.addPage(
|
||||
MultiPage(
|
||||
build: (context) => [
|
||||
buildHeader(image, searchDateFormatted),
|
||||
SizedBox(height: 1 * PdfPageFormat.cm),
|
||||
buildInvoice(itemSales),
|
||||
Divider(),
|
||||
SizedBox(height: 0.25 * PdfPageFormat.cm),
|
||||
],
|
||||
footer: (context) => buildFooter(),
|
||||
),
|
||||
);
|
||||
|
||||
return HelperPdfService.saveDocument(
|
||||
name:
|
||||
'Enaklo POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
pdf: pdf);
|
||||
}
|
||||
|
||||
static Widget buildHeader(MemoryImage image, String searchDateFormatted) =>
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 1 * PdfPageFormat.cm),
|
||||
Text('Enaklo POS | Item Sales Report',
|
||||
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 buildInvoice(List<ItemSales> itemSales) {
|
||||
final headers = ['Id', 'Order', 'Product', 'Qty', 'Price', 'Total'];
|
||||
final data = itemSales.map((item) {
|
||||
return [
|
||||
item.id!,
|
||||
item.orderId,
|
||||
item.productName,
|
||||
item.price!.currencyFormatRp,
|
||||
item.quantity,
|
||||
(item.price! * item.quantity!).currencyFormatRp
|
||||
];
|
||||
}).toList();
|
||||
|
||||
return Table.fromTextArray(
|
||||
headers: headers,
|
||||
data: data,
|
||||
border: null,
|
||||
headerStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: PdfColor.fromHex('FFFFFF')),
|
||||
headerDecoration: BoxDecoration(color: PdfColors.blue),
|
||||
cellHeight: 30,
|
||||
cellAlignments: {
|
||||
0: Alignment.centerLeft,
|
||||
1: Alignment.center,
|
||||
2: Alignment.center,
|
||||
3: Alignment.centerLeft,
|
||||
4: Alignment.centerLeft,
|
||||
5: Alignment.centerLeft,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget buildFooter() => 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,
|
||||
bool unite = false,
|
||||
}) {
|
||||
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(title, style: style)),
|
||||
Text(value, style: unite ? style : null),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PermessionHelper {
|
||||
Future<bool> checkPermission() async {
|
||||
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||
bool permissionStatus;
|
||||
if (deviceInfo.version.sdkInt > 32) {
|
||||
permissionStatus = await Permission.photos.request().isGranted;
|
||||
} else {
|
||||
permissionStatus = await Permission.storage.request().isGranted;
|
||||
}
|
||||
|
||||
if (permissionStatus) {
|
||||
log('Izin penyimpanan sudah diberikan.');
|
||||
} else {
|
||||
if (deviceInfo.version.sdkInt > 32) {
|
||||
log('deviceInfo.version.sdkInt > 32.');
|
||||
permissionStatus = await Permission.photos.request().isGranted;
|
||||
} else {
|
||||
permissionStatus = await Permission.storage.request().isGranted;
|
||||
}
|
||||
// } else {
|
||||
// openAppSettings();
|
||||
// }
|
||||
}
|
||||
log('permissionStatus: $permissionStatus');
|
||||
return permissionStatus;
|
||||
}
|
||||
|
||||
void permessionPrinter() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.bluetooth,
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothAdvertise,
|
||||
Permission.bluetoothConnect,
|
||||
].request();
|
||||
|
||||
log("statuses: $statuses");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_esc_pos_network/flutter_esc_pos_network.dart';
|
||||
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
|
||||
import 'package:enaklo_pos/data/models/response/print_model.dart';
|
||||
|
||||
class PrinterService {
|
||||
static final PrinterService _instance = PrinterService._internal();
|
||||
factory PrinterService() => _instance;
|
||||
PrinterService._internal();
|
||||
|
||||
/// Connect to Bluetooth printer
|
||||
Future<bool> connectBluetoothPrinter(String macAddress) async {
|
||||
try {
|
||||
// Check if already connected
|
||||
bool isConnected = await PrintBluetoothThermal.connectionStatus;
|
||||
if (isConnected) {
|
||||
log("Already connected to Bluetooth printer");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Connect to the printer
|
||||
bool connected = await PrintBluetoothThermal.connect(
|
||||
macPrinterAddress: macAddress);
|
||||
|
||||
if (connected) {
|
||||
log("Successfully connected to Bluetooth printer: $macAddress");
|
||||
} else {
|
||||
log("Failed to connect to Bluetooth printer: $macAddress");
|
||||
}
|
||||
|
||||
return connected;
|
||||
} catch (e) {
|
||||
log("Error connecting to Bluetooth printer: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Print using Bluetooth printer
|
||||
Future<bool> printBluetooth(List<int> printData) async {
|
||||
try {
|
||||
bool isConnected = await PrintBluetoothThermal.connectionStatus;
|
||||
if (!isConnected) {
|
||||
log("Not connected to Bluetooth printer");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool printResult = await PrintBluetoothThermal.writeBytes(printData);
|
||||
if (printResult) {
|
||||
log("Successfully printed via Bluetooth");
|
||||
} else {
|
||||
log("Failed to print via Bluetooth");
|
||||
}
|
||||
|
||||
return printResult;
|
||||
} catch (e) {
|
||||
log("Error printing via Bluetooth: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Print using Network printer
|
||||
Future<bool> printNetwork(String ipAddress, List<int> printData) async {
|
||||
try {
|
||||
final printer = PrinterNetworkManager(ipAddress);
|
||||
PosPrintResult connect = await printer.connect();
|
||||
|
||||
if (connect == PosPrintResult.success) {
|
||||
PosPrintResult printing = await printer.printTicket(printData);
|
||||
printer.disconnect();
|
||||
|
||||
if (printing == PosPrintResult.success) {
|
||||
log("Successfully printed via Network printer: $ipAddress");
|
||||
return true;
|
||||
} else {
|
||||
log("Failed to print via Network printer: ${printing.msg}");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log("Failed to connect to Network printer: ${connect.msg}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
log("Error printing via Network: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Print with automatic printer type detection
|
||||
Future<bool> printWithPrinter(PrintModel printer, List<int> printData, BuildContext context) async {
|
||||
try {
|
||||
if (printer.type == 'Bluetooth') {
|
||||
bool connected = await connectBluetoothPrinter(printer.address);
|
||||
if (!connected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to connect to ${printer.name}')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool printResult = await printBluetooth(printData);
|
||||
if (!printResult) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to print to ${printer.name}')),
|
||||
);
|
||||
}
|
||||
return printResult;
|
||||
} else {
|
||||
bool printResult = await printNetwork(printer.address, printData);
|
||||
if (!printResult) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to print to ${printer.name}')),
|
||||
);
|
||||
}
|
||||
return printResult;
|
||||
}
|
||||
} catch (e) {
|
||||
log("Error printing with printer ${printer.name}: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error printing to ${printer.name}: $e')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect from Bluetooth printer
|
||||
Future<bool> disconnectBluetooth() async {
|
||||
try {
|
||||
bool result = await PrintBluetoothThermal.disconnect;
|
||||
log("Bluetooth printer disconnected: $result");
|
||||
return result;
|
||||
} catch (e) {
|
||||
log("Error disconnecting Bluetooth printer: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if Bluetooth is enabled
|
||||
Future<bool> isBluetoothEnabled() async {
|
||||
try {
|
||||
return await PrintBluetoothThermal.bluetoothEnabled;
|
||||
} catch (e) {
|
||||
log("Error checking Bluetooth status: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get paired Bluetooth devices
|
||||
Future<List<BluetoothInfo>> getPairedBluetoothDevices() async {
|
||||
try {
|
||||
return await PrintBluetoothThermal.pairedBluetooths;
|
||||
} catch (e) {
|
||||
log("Error getting paired Bluetooth devices: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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");
|
||||
|
||||
final pdf = Document();
|
||||
log("PDF document created");
|
||||
|
||||
// Load logo image
|
||||
log("Loading logo image...");
|
||||
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
|
||||
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:
|
||||
'Enaklo POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
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),
|
||||
Text('Enaklo POS | Summary Sales Report',
|
||||
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()}");
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
value: "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
|
||||
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,
|
||||
),
|
||||
value: safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
|
||||
import 'package:enaklo_pos/core/extensions/int_ext.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
|
||||
import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart';
|
||||
import 'package:pdf/widgets.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
class TransactionSalesInvoice {
|
||||
static late Font ttf;
|
||||
static Future<File> generate(
|
||||
List<ItemOrder> itemOrders, String searchDateFormatted) async {
|
||||
final pdf = Document();
|
||||
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
|
||||
// ttf = Font.ttf(data);
|
||||
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
|
||||
final Uint8List bytes = dataImage.buffer.asUint8List();
|
||||
|
||||
// Membuat objek Image dari gambar
|
||||
final image = pw.MemoryImage(bytes);
|
||||
|
||||
pdf.addPage(
|
||||
MultiPage(
|
||||
build: (context) => [
|
||||
buildHeader(image, searchDateFormatted),
|
||||
SizedBox(height: 1 * PdfPageFormat.cm),
|
||||
buildInvoice(itemOrders),
|
||||
Divider(),
|
||||
SizedBox(height: 0.25 * PdfPageFormat.cm),
|
||||
],
|
||||
footer: (context) => buildFooter(),
|
||||
),
|
||||
);
|
||||
|
||||
return HelperPdfService.saveDocument(
|
||||
name:
|
||||
'Enaklo POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
pdf: pdf);
|
||||
}
|
||||
|
||||
static Widget buildHeader(MemoryImage image, String searchDateFormatted) =>
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 1 * PdfPageFormat.cm),
|
||||
Text('Enaklo POS | Transaction Sales Report',
|
||||
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 buildInvoice(List<ItemOrder> itemOrders) {
|
||||
final headers = [
|
||||
'Total',
|
||||
'Sub Total',
|
||||
'Tax',
|
||||
'Discount',
|
||||
'Service',
|
||||
'Time'
|
||||
];
|
||||
final data = itemOrders.map((item) {
|
||||
return [
|
||||
item.total!.currencyFormatRp,
|
||||
item.subTotal!.currencyFormatRp,
|
||||
item.tax!.currencyFormatRp,
|
||||
int.parse(item.discountAmount!.replaceAll('.00', '')).currencyFormatRp,
|
||||
item.serviceCharge!.currencyFormatRp,
|
||||
item.transactionTime!.toFormattedDate2(),
|
||||
];
|
||||
}).toList();
|
||||
|
||||
return Table.fromTextArray(
|
||||
headers: headers,
|
||||
data: data,
|
||||
border: null,
|
||||
headerStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: PdfColor.fromHex('FFFFFF')),
|
||||
headerDecoration: BoxDecoration(color: PdfColors.blue),
|
||||
cellHeight: 30,
|
||||
cellAlignments: {
|
||||
0: Alignment.center,
|
||||
1: Alignment.center,
|
||||
2: Alignment.center,
|
||||
3: Alignment.center,
|
||||
4: Alignment.center,
|
||||
5: Alignment.center,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget buildFooter() => 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,
|
||||
bool unite = false,
|
||||
}) {
|
||||
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(title, style: style)),
|
||||
Text(value, style: unite ? style : null),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user