first commit
This commit is contained in:
58
lib/service/check_actions_when_no_branch.dart
Normal file
58
lib/service/check_actions_when_no_branch.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import '../Screens/branch/branch_list.dart';
|
||||
|
||||
Future<bool> checkActionWhenNoBranch({required BuildContext context, required WidgetRef ref, String? actionName}) async {
|
||||
final businessInfo = await ref.watch(businessInfoProvider.future);
|
||||
if ((businessInfo.data?.addons?.multiBranchAddon == null) || (businessInfo.data?.addons?.multiBranchAddon == false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((businessInfo.data?.addons?.multiBranchAddon == true) && (businessInfo.data?.branchCount ?? 0) < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actionName != null) {
|
||||
switch (actionName.toLowerCase()) {
|
||||
case 'sale':
|
||||
break;
|
||||
case 'pos sale':
|
||||
break;
|
||||
case 'purchase':
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (businessInfo.data?.user?.activeBranchId == null && businessInfo.data?.user?.branchId == null) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"You have to switch a branch for this action.",
|
||||
style: TextStyle(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
BranchListWidget(formFullPage: false),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
272
lib/service/check_user_role_permission_provider.dart
Normal file
272
lib/service/check_user_role_permission_provider.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../Provider/profile_provider.dart';
|
||||
import '../model/business_info_model.dart';
|
||||
|
||||
class UserPermissionNotifier extends StateNotifier<List<String>> {
|
||||
final Ref ref;
|
||||
late final ProviderSubscription<AsyncValue<BusinessInformationModel>> _subscription;
|
||||
|
||||
bool _visibilityIsNull = false;
|
||||
|
||||
UserPermissionNotifier(this.ref) : super([]) {
|
||||
_subscription = ref.listen<AsyncValue<BusinessInformationModel>>(
|
||||
businessInfoProvider,
|
||||
(previous, next) {
|
||||
next.whenData((businessInfo) {
|
||||
final user = businessInfo.data?.user;
|
||||
if (user != null) {
|
||||
_visibilityIsNull = user.visibilityIsNull;
|
||||
state = user.getAllPermissions();
|
||||
} else {
|
||||
_visibilityIsNull = false;
|
||||
state = [];
|
||||
}
|
||||
});
|
||||
if (next.hasError) {
|
||||
_visibilityIsNull = false;
|
||||
state = [];
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
}
|
||||
|
||||
bool get visibilityIsNull => _visibilityIsNull;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool has(String permission) => state.contains(permission);
|
||||
}
|
||||
|
||||
final userPermissionProvider = StateNotifierProvider<UserPermissionNotifier, List<String>>(
|
||||
(ref) => UserPermissionNotifier(ref),
|
||||
);
|
||||
|
||||
class PermissionService {
|
||||
final WidgetRef ref;
|
||||
PermissionService(this.ref);
|
||||
|
||||
bool hasPermission(String permission, {BuildContext? context}) {
|
||||
final permissions = ref.read(userPermissionProvider);
|
||||
final visibilityIsNull = ref.read(userPermissionProvider.notifier).visibilityIsNull;
|
||||
|
||||
if (visibilityIsNull) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (permissions.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final result = permissions.contains(permission);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasAnyPermission(List<String> permissions) {
|
||||
final userPermissions = ref.read(userPermissionProvider);
|
||||
final visibilityIsNull = ref.read(userPermissionProvider.notifier).visibilityIsNull;
|
||||
|
||||
if (visibilityIsNull) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userPermissions.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return permissions.any((permission) => userPermissions.contains(permission));
|
||||
}
|
||||
}
|
||||
|
||||
enum Permit {
|
||||
dashboardRead('dashboard.read'),
|
||||
salesRead('sales.read'),
|
||||
salesCreate('sales.create'),
|
||||
salesUpdate('sales.update'),
|
||||
salesDelete('sales.delete'),
|
||||
salesPriceView('sales.price'),
|
||||
inventoryRead('inventory.read'),
|
||||
inventoryCreate('inventory.create'),
|
||||
inventoryPriceView('inventory.price'),
|
||||
saleReturnsRead('sale-returns.read'),
|
||||
saleReturnsCreate('sale-returns.create'),
|
||||
saleReturnsPriceView('sale-returns.price'),
|
||||
purchasesRead('purchases.read'),
|
||||
purchasesCreate('purchases.create'),
|
||||
purchasesUpdate('purchases.update'),
|
||||
purchasesDelete('purchases.delete'),
|
||||
purchasesPriceView('purchases.price'),
|
||||
purchaseReturnsRead('purchase-returns.read'),
|
||||
purchaseReturnsCreate('purchase-returns.create'),
|
||||
purchaseReturnPriceView('purchase-returns.price'),
|
||||
productsRead('products.read'),
|
||||
productsCreate('products.create'),
|
||||
productsUpdate('products.update'),
|
||||
productsDelete('products.delete'),
|
||||
productsPriceView('products.price'),
|
||||
branchesRead('branches.read'),
|
||||
branchesCreate('branches.create'),
|
||||
branchesUpdate('branches.update'),
|
||||
branchesDelete('branches.delete'),
|
||||
productsExpiredRead('products-expired.read'),
|
||||
barcodesRead('barcodes.read'),
|
||||
barcodesCreate('barcodes.create'),
|
||||
bulkUploadsRead('bulk-uploads.read'),
|
||||
bulkUploadsCreate('bulk-uploads.create'),
|
||||
categoriesRead('categories.read'),
|
||||
categoriesCreate('categories.create'),
|
||||
categoriesUpdate('categories.update'),
|
||||
categoriesDelete('categories.delete'),
|
||||
brandsRead('brands.read'),
|
||||
brandsCreate('brands.create'),
|
||||
brandsUpdate('brands.update'),
|
||||
brandsDelete('brands.delete'),
|
||||
unitsRead('units.read'),
|
||||
unitsCreate('units.create'),
|
||||
unitsUpdate('units.update'),
|
||||
unitsDelete('units.delete'),
|
||||
productModelsRead('product-models.read'),
|
||||
productModelsCreate('product-models.create'),
|
||||
productModelsUpdate('product-models.update'),
|
||||
productModelsDelete('product-models.delete'),
|
||||
stocksRead('stocks.read'),
|
||||
stocksPriceView('stocks.price'),
|
||||
expiredProductsRead('expired-products.read'),
|
||||
partiesRead('parties.read'),
|
||||
partiesCreate('parties.create'),
|
||||
partiesUpdate('parties.update'),
|
||||
partiesDelete('parties.delete'),
|
||||
partiesPriceView('parties.price'),
|
||||
incomesRead('incomes.read'),
|
||||
incomesCreate('incomes.create'),
|
||||
incomesUpdate('incomes.update'),
|
||||
incomesDelete('incomes.delete'),
|
||||
incomesPriceView('incomes.price'),
|
||||
incomeCategoriesRead('income-categories.read'),
|
||||
incomeCategoriesCreate('income-categories.create'),
|
||||
incomeCategoriesUpdate('income-categories.update'),
|
||||
incomeCategoriesDelete('income-categories.delete'),
|
||||
expensesRead('expenses.read'),
|
||||
expensesCreate('expenses.create'),
|
||||
expensesUpdate('expenses.update'),
|
||||
expensesDelete('expenses.delete'),
|
||||
expensesPriceView('expenses.price'),
|
||||
expenseCategoriesRead('expense-categories.read'),
|
||||
expenseCategoriesCreate('expense-categories.create'),
|
||||
expenseCategoriesUpdate('expense-categories.update'),
|
||||
expenseCategoriesDelete('expense-categories.delete'),
|
||||
vatsRead('vats.read'),
|
||||
vatsCreate('vats.create'),
|
||||
vatsUpdate('vats.update'),
|
||||
vatsDelete('vats.delete'),
|
||||
duesRead('dues.read'),
|
||||
subscriptionsRead('subscriptions.read'),
|
||||
lossProfitsRead('loss-profits.read'),
|
||||
paymentTypesRead('payment-types.read'),
|
||||
paymentTypesCreate('payment-types.create'),
|
||||
paymentTypesUpdate('payment-types.update'),
|
||||
paymentTypesDelete('payment-types.delete'),
|
||||
rolesRead('roles.read'),
|
||||
rolesCreate('roles.create'),
|
||||
rolesUpdate('roles.update'),
|
||||
rolesDelete('roles.delete'),
|
||||
departmentRead('department.read'),
|
||||
departmentCreate('department.create'),
|
||||
departmentUpdate('department.update'),
|
||||
departmentDelete('department.delete'),
|
||||
designationsRead('designations.read'),
|
||||
designationsCreate('designations.create'),
|
||||
designationsUpdate('designations.update'),
|
||||
designationsDelete('designations.delete'),
|
||||
shiftsRead('shifts.read'),
|
||||
shiftsCreate('shifts.create'),
|
||||
shiftsUpdate('shifts.update'),
|
||||
shiftsDelete('shifts.delete'),
|
||||
employeesRead('employees.read'),
|
||||
employeesCreate('employees.create'),
|
||||
employeesUpdate('employees.update'),
|
||||
employeesDelete('employees.delete'),
|
||||
leaveTypesRead('leave-types.read'),
|
||||
leaveTypesCreate('leave-types.create'),
|
||||
leaveTypesUpdate('leave-types.update'),
|
||||
leaveTypesDelete('leave-types.delete'),
|
||||
leavesRead('leaves.read'),
|
||||
leavesCreate('leaves.create'),
|
||||
leavesUpdate('leaves.update'),
|
||||
leavesDelete('leaves.delete'),
|
||||
holidaysRead('holidays.read'),
|
||||
holidaysCreate('holidays.create'),
|
||||
holidaysUpdate('holidays.update'),
|
||||
holidaysDelete('holidays.delete'),
|
||||
attendancesRead('attendances.read'),
|
||||
attendancesCreate('attendances.create'),
|
||||
attendancesUpdate('attendances.update'),
|
||||
attendancesDelete('attendances.delete'),
|
||||
payrollsRead('payrolls.read'),
|
||||
payrollsCreate('payrolls.create'),
|
||||
payrollsUpdate('payrolls.update'),
|
||||
payrollsDelete('payrolls.delete'),
|
||||
attendanceReportsRead('attendance-reports.read'),
|
||||
payrollReportsRead('payroll-reports.read'),
|
||||
leaveReportsRead('leave-reports.read'),
|
||||
warehousesRead('warehouses.read'),
|
||||
warehousesCreate('warehouses.create'),
|
||||
warehousesUpdate('warehouses.update'),
|
||||
warehousesDelete('warehouses.delete'),
|
||||
transfersRead('transfers.read'),
|
||||
transfersCreate('transfers.create'),
|
||||
transfersUpdate('transfers.update'),
|
||||
transfersDelete('transfers.delete'),
|
||||
racksRead('racks.read'),
|
||||
racksCreate('racks.create'),
|
||||
racksUpdate('racks.update'),
|
||||
racksDelete('racks.delete'),
|
||||
shelfsRead('shelfs.read'),
|
||||
shelfsCreate('shelfs.create'),
|
||||
shelfsUpdate('shelfs.update'),
|
||||
shelfsDelete('shelfs.delete'),
|
||||
manageSettingsRead('manage-settings.read'),
|
||||
manageSettingsUpdate('manage-settings.update'),
|
||||
downloadApkRead('download-apk.read'),
|
||||
saleReportsRead('sale-reports.read'),
|
||||
saleReturnReportsRead('sale-return-reports.read'),
|
||||
purchaseReportsRead('purchase-reports.read'),
|
||||
purchaseReturnReportsRead('purchase-return-reports.read'),
|
||||
vatReportsRead('vat-reports.read'),
|
||||
incomeReportsRead('income-reports.read'),
|
||||
expenseReportsRead('expense-reports.read'),
|
||||
lossProfitsDetailsRead('loss-profits-details.read'),
|
||||
stockReportsRead('stock-reports.read'),
|
||||
dueReportsRead('due-reports.read'),
|
||||
supplierDueReportsRead('supplier-due-reports.read'),
|
||||
lossProfitReportsRead('loss-profit-reports.read'),
|
||||
transactionHistoryReportsRead('transaction-history-reports.read'),
|
||||
subscriptionReportsRead('subscription-reports.read'),
|
||||
expiredProductReportsRead('expired-product-reports.read'),
|
||||
dayBookReportsRead('day-book-reports.read'),
|
||||
billWiseProfitRead('bill-wise-profit.read'),
|
||||
cashflowRead('cashflow.read'),
|
||||
balanceSheetRead('balance-sheet.read'),
|
||||
taxReportRead('tax-report.read'),
|
||||
customerLedgerRead('customer-ledger.read'),
|
||||
supplierLedgerRead('supplier-ledger.read'),
|
||||
parityWiseProfitRead('parity-wise-profit.read'),
|
||||
top5CustomerRead('top-5-customer.read'),
|
||||
top5SupplierRead('top-5-supplier.read'),
|
||||
comboReportRead('combo-report.read'),
|
||||
top5ProductRead('top-5-product.read'),
|
||||
productWiseProfitLossRead('product-wise-profit-loss.read'),
|
||||
productPurchaseReportRead('product-purchase-report.read'),
|
||||
productSalesReportRead('product-sales-report.read'),
|
||||
productPurchaseHistoryRead('product-purchase-history.read'),
|
||||
productSaleHistoryRead('product-sale-history.read');
|
||||
|
||||
final String value;
|
||||
const Permit(this.value);
|
||||
}
|
||||
1
lib/service/services.dart
Normal file
1
lib/service/services.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'thermal_print/_thermal_print_service.dart';
|
||||
3
lib/service/thermal_print/_thermal_print_service.dart
Normal file
3
lib/service/thermal_print/_thermal_print_service.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
library;
|
||||
|
||||
export 'src/templates/templates.dart';
|
||||
@@ -0,0 +1,304 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class DueThermalInvoiceTemplate {
|
||||
DueThermalInvoiceTemplate({
|
||||
required this.printDueTransactionModel,
|
||||
required this.is58mm,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final PrintDueTransactionModel printDueTransactionModel;
|
||||
final bool is58mm;
|
||||
final BuildContext context;
|
||||
final bool isRTL;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
Future<List<int>> get template async {
|
||||
final _lang = lang.S.of(context);
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
final _imageBytes = await _generateLayout(_lang);
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate receipt.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout(lang.S _lang) async {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
final info = printDueTransactionModel.personalInformationModel.data;
|
||||
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (info?.thermalInvoiceLogo != null && info?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
"${APIConfig.domain}${info?.thermalInvoiceLogo}",
|
||||
width: is58mm ? 120 : 184,
|
||||
height: is58mm ? 120 : 184,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (info?.invoiceScannerLogo != null && info?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + info!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Payment Labels
|
||||
final paymentLabels = _buildPaymentLabels();
|
||||
|
||||
// 3. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
widgets: [
|
||||
// --- Header ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (info?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
info?.companyName ?? '',
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (data?.branch?.name != null)
|
||||
thermer.ThermerText('Branch: ${data?.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (info?.meta?.showAddress == 1)
|
||||
if (data?.branch?.address != null || info?.address != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.address}: ${data?.branch?.address ?? info?.address ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showPhoneNumber == 1)
|
||||
if (data?.branch?.phone != null || info?.phoneNumber != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${data?.branch?.phone ?? info?.phoneNumber ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showVat == 1)
|
||||
if (info?.vatNo != null && info?.meta?.showVat == 1)
|
||||
thermer.ThermerText(
|
||||
"${info?.vatName ?? _lang.vatNumber}: ${info?.vatNo}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.receipt, // Due collection is usually a Receipt
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Data Table (Single Row for Due Context) ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.invoice, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.dueAmount, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: [
|
||||
thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('1', style: _commonStyle()),
|
||||
thermer.ThermerText(data?.invoiceNumber ?? '', style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(data?.totalDue ?? 0, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
])
|
||||
],
|
||||
cellWidths: is58mm ? {0: null, 1: 0.3} : {0: 0.1, 1: null, 2: 0.3},
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Calculations ---
|
||||
_buildCalculationColumn(_lang),
|
||||
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Payment Info ---
|
||||
thermer.ThermerText(
|
||||
"${_lang.paidVia} : ${paymentLabels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.left,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Footer ---
|
||||
if (info?.gratitudeMessage != null && info?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(info?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (data?.paymentDate != null)
|
||||
thermer.ThermerText(
|
||||
DateFormat('M/d/yyyy h:mm a').format(DateTime.parse(data!.paymentDate!)),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
if (info?.showNote == 1)
|
||||
thermer.ThermerText(
|
||||
'${info?.invoiceNoteLevel ?? _lang.note}: ${info?.invoiceNote ?? ''}',
|
||||
textAlign: thermer.TextAlign.left,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
|
||||
if (info?.developBy != null)
|
||||
thermer.ThermerText(
|
||||
'${info?.developByLevel ?? _lang.developedBy}: ${info?.developBy}',
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter Space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
final dateStr = data?.paymentDate != null ? DateFormat.yMd().format(DateTime.parse(data!.paymentDate!)) : '';
|
||||
final timeStr = data?.paymentDate != null ? DateFormat.jm().format(DateTime.parse(data!.paymentDate!)) : '';
|
||||
|
||||
final receiptText = '${_lang.receipt}: ${data?.invoiceNumber ?? 'Not Provided'}';
|
||||
final dateText = '${_lang.date}: $dateStr';
|
||||
final timeText = '${_lang.time}: $timeStr';
|
||||
final nameText = '${_lang.receivedFrom}: ${data?.party?.name ?? ''}';
|
||||
final mobileText = '${_lang.mobile} ${data?.party?.phone ?? ''}';
|
||||
final receivedByText =
|
||||
'${_lang.receivedBy}: ${data?.user?.role == "shop-owner" ? 'Admin' : data?.user?.name ?? ''}';
|
||||
|
||||
if (is58mm) {
|
||||
// 58mm: Stacked
|
||||
return [
|
||||
thermer.ThermerText(receiptText, style: _commonStyle()),
|
||||
if (data?.paymentDate != null) thermer.ThermerText("$dateText $timeStr", style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(receivedByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
// 80mm: Two Columns
|
||||
return [
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(receiptText, style: _commonStyle()),
|
||||
if (data?.paymentDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(receivedByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(children: [
|
||||
calcRow('${_lang.totalDue}:', data?.totalDue ?? 0),
|
||||
calcRow('${_lang.paymentsAmount}:', data?.payDueAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.remainingDue}:', data?.dueAmountAfterPay ?? 0, bold: true),
|
||||
]);
|
||||
}
|
||||
|
||||
List<String> _buildPaymentLabels() {
|
||||
final transactions = printDueTransactionModel.dueTransactionModel?.transactions ?? [];
|
||||
List<String> labels = [];
|
||||
|
||||
for (var item in transactions) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = lang.S.of(context).cash;
|
||||
if (item.transactionType == 'cheque_payment') label = lang.S.of(context).cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = lang.S.of(context).wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Products/add%20product/modle/create_product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Purchase/Model/purchase_transaction_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
|
||||
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class PurchaseThermalInvoiceTemplate {
|
||||
PurchaseThermalInvoiceTemplate({
|
||||
required this.printTransactionModel,
|
||||
required this.productList,
|
||||
required this.is58mm,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final PrintPurchaseTransactionModel printTransactionModel;
|
||||
final List<PurchaseDetails>? productList;
|
||||
final bool is58mm;
|
||||
final BuildContext context;
|
||||
final bool isRTL;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Data Logic (Adapted from your provided code) ---
|
||||
|
||||
num _getProductPrice(num detailsId) {
|
||||
return productList!.where((element) => element.id == detailsId).first.productPurchasePrice ?? 0;
|
||||
}
|
||||
|
||||
String _getProductName(num detailsId) {
|
||||
final details = printTransactionModel.purchaseTransitionModel?.details?.firstWhere(
|
||||
(element) => element.id == detailsId,
|
||||
orElse: () => PurchaseDetails(),
|
||||
);
|
||||
String name = details?.product?.productName ?? '';
|
||||
if (details?.product?.productType == ProductType.variant.name) {
|
||||
name += ' [${details?.stock?.batchNo ?? ''}]';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
num _getProductQuantity(num detailsId) {
|
||||
num totalQuantity = productList?.where((element) => element.id == detailsId).first.quantities ?? 0;
|
||||
|
||||
// Add returned quantities logic
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
if (details.purchaseDetailId == detailsId) {
|
||||
totalQuantity += details.returnQty ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
num _getTotalForOldInvoice() {
|
||||
num total = 0;
|
||||
if (productList != null) {
|
||||
for (var element in productList!) {
|
||||
num productPrice = element.productPurchasePrice ?? 0;
|
||||
num productQuantity = _getProductQuantity(element.id ?? 0);
|
||||
total += productPrice * productQuantity;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
num _getReturnedDiscountAmount() {
|
||||
num totalReturnDiscount = 0;
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
totalReturnDiscount += ((_getProductPrice(details.purchaseDetailId ?? 0) * (details.returnQty ?? 0)) -
|
||||
((details.returnAmount ?? 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalReturnDiscount;
|
||||
}
|
||||
|
||||
num _getTotalReturnedAmount() {
|
||||
num totalReturn = 0;
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
totalReturn += details.returnAmount ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalReturn;
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
Future<List<int>> get template async {
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
final _imageBytes = await _generateLayout();
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate invoice.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout() async {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
final info = printTransactionModel.personalInformationModel.data;
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (info?.thermalInvoiceLogo != null && info?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
"${APIConfig.domain}${info?.thermalInvoiceLogo}",
|
||||
width: is58mm ? 120 : 184,
|
||||
height: is58mm ? 120 : 184,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (info?.invoiceScannerLogo != null && info?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + info!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Product Rows
|
||||
final productRows = _buildProductRows();
|
||||
|
||||
// 3. Prepare Returns
|
||||
final returnWidgets = _buildReturnSection(_lang);
|
||||
|
||||
// 4. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
widgets: [
|
||||
// --- Header ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (info?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
info?.companyName ?? '',
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (data?.branch?.name != null)
|
||||
thermer.ThermerText('${_lang.branch}: ${data?.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (info?.meta?.showAddress == 1)
|
||||
if (data?.branch?.address != null || info?.address != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.address}: ${data?.branch?.address ?? info?.address ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showPhoneNumber == 1)
|
||||
if (data?.branch?.phone != null || info?.phoneNumber != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${data?.branch?.phone ?? info?.phoneNumber ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showVat == 1)
|
||||
if (info?.vatNo != null && info?.meta?.showVat == 1)
|
||||
thermer.ThermerText(
|
||||
"${info?.vatName ?? _lang.vatNumber}: ${info?.vatNo}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.invoice,
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Product Table ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.price, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.total, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: productRows,
|
||||
cellWidths: is58mm
|
||||
? {0: null, 1: 0.2, 2: 0.2, 3: 0.25} // 58mm
|
||||
: {0: 0.1, 1: null, 2: 0.15, 3: 0.15, 4: 0.2}, // 80mm
|
||||
columnSpacing: 10.0,
|
||||
rowSpacing: 3.0,
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Calculations ---
|
||||
if (!is58mm)
|
||||
thermer.ThermerRow(
|
||||
children: [
|
||||
thermer.ThermerExpanded(flex: 4, child: thermer.ThermerAlign(child: _buildPaymentInfoText(_lang))),
|
||||
thermer.ThermerExpanded(flex: 6, child: _buildCalculationColumn(_lang)),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
_buildCalculationColumn(_lang),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
_buildPaymentInfoText(_lang),
|
||||
],
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Returns ---
|
||||
...returnWidgets,
|
||||
|
||||
// --- Footer ---
|
||||
if (info?.gratitudeMessage != null && info?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(info?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (data?.purchaseDate != null)
|
||||
thermer.ThermerText(
|
||||
DateFormat('M/d/yyyy h:mm a').format(DateTime.parse(data!.purchaseDate!)),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
if (info?.showNote == 1)
|
||||
thermer.ThermerText(
|
||||
'${info?.invoiceNoteLevel ?? _lang.note}: ${info?.invoiceNote ?? ''}',
|
||||
textAlign: thermer.TextAlign.left,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
// if (info?.developByLink != null)
|
||||
// thermer.ThermerAlign(child: thermer.ThermerQRCode(data: info?.developByLink ?? '', size: 120)),
|
||||
|
||||
if (info?.developBy != null)
|
||||
thermer.ThermerText(
|
||||
'${info?.developByLevel ?? _lang.developedBy}: ${info?.developBy}',
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter Space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
final dateStr = data?.purchaseDate != null ? DateFormat.yMd().format(DateTime.parse(data!.purchaseDate!)) : '';
|
||||
final timeStr = data?.purchaseDate != null ? DateFormat.jm().format(DateTime.parse(data!.purchaseDate!)) : '';
|
||||
|
||||
final invText = '${_lang.invoice}: ${data?.invoiceNumber ?? 'Not Provided'}';
|
||||
final dateText = '${_lang.date}: $dateStr';
|
||||
final timeText = '${_lang.time}: $timeStr';
|
||||
final nameText = '${_lang.name}: ${data?.party?.name ?? 'Guest'}';
|
||||
final mobileText = '${_lang.mobile} ${data?.party?.phone ?? ''}';
|
||||
final purchaseByText = '${_lang.purchaseBy} ${data?.user?.role == "shop-owner" ? 'Admin' : data?.user?.name ?? ''}';
|
||||
|
||||
if (is58mm) {
|
||||
return [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (data?.purchaseDate != null) thermer.ThermerText("$dateText $timeStr", style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(purchaseByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (data?.purchaseDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(purchaseByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
List<thermer.ThermerTableRow> _buildProductRows() {
|
||||
List<thermer.ThermerTableRow> rows = [];
|
||||
if (productList == null) return rows;
|
||||
|
||||
for (var index = 0; index < productList!.length; index++) {
|
||||
final item = productList![index];
|
||||
final qty = _getProductQuantity(item.id ?? 0);
|
||||
final price = item.productPurchasePrice ?? 0;
|
||||
final amount = price * qty;
|
||||
|
||||
rows.add(thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('${index + 1}', style: _commonStyle()),
|
||||
thermer.ThermerText(_getProductName(item.id ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(qty, addComma: true),
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(price, addComma: true),
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(amount, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(children: [
|
||||
calcRow('${_lang.subTotal}:', _getTotalForOldInvoice()),
|
||||
calcRow('${_lang.discount}:', (data?.discountAmount ?? 0) + _getReturnedDiscountAmount()),
|
||||
calcRow('${data?.vat?.name ?? _lang.vat}:', data?.vatAmount ?? 0),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
if (_getTotalReturnedAmount() > 0) calcRow('${_lang.returnAmount}:', _getTotalReturnedAmount()),
|
||||
calcRow('${_lang.totalPayable}:', data?.totalAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.paidAmount}:', ((data?.totalAmount ?? 0) - (data?.dueAmount ?? 0)) + (data?.changeAmount ?? 0)),
|
||||
if ((data?.dueAmount ?? 0) > 0) calcRow('${_lang.dueAmount}', data?.dueAmount ?? 0),
|
||||
if ((data?.changeAmount ?? 0) > 0) calcRow('${_lang.changeAmount}:', data?.changeAmount ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
thermer.ThermerText _buildPaymentInfoText(lang.S _lang) {
|
||||
final transactions = printTransactionModel.purchaseTransitionModel?.transactions ?? [];
|
||||
List<String> labels = [];
|
||||
|
||||
for (var item in transactions) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = _lang.cash;
|
||||
if (item.transactionType == 'cheque_payment') label = _lang.cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = _lang.wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
return thermer.ThermerText(
|
||||
"${_lang.paidVia}: ${labels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: is58mm ? thermer.TextAlign.left : thermer.TextAlign.left,
|
||||
);
|
||||
}
|
||||
|
||||
List<thermer.ThermerWidget> _buildReturnSection(lang.S _lang) {
|
||||
final returns = printTransactionModel.purchaseTransitionModel?.purchaseReturns;
|
||||
if (returns?.isEmpty ?? true) return [];
|
||||
|
||||
List<thermer.ThermerWidget> widgets = [];
|
||||
List<String> processedDates = [];
|
||||
|
||||
for (var i = 0; i < (returns?.length ?? 0); i++) {
|
||||
final dateStr = returns![i].returnDate?.substring(0, 10);
|
||||
if (dateStr != null && !processedDates.contains(dateStr)) {
|
||||
processedDates.add(dateStr);
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
|
||||
// Return Header
|
||||
widgets.add(thermer.ThermerRow(
|
||||
children: [
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText('${_lang.retur}-${DateFormat.yMd().format(DateTime.parse(returns[i].returnDate!))}',
|
||||
style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.total, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
],
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerTable(
|
||||
data: (returns[i].purchaseReturnDetails ?? []).map((d) {
|
||||
// Re-using index logic might be tricky here for SL, simplify if needed
|
||||
return thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('*', style: _commonStyle()), // Bullet for return items or dynamic index
|
||||
thermer.ThermerText(_getProductName(d.purchaseDetailId ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText('${d.returnQty ?? 0}', textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(d.returnAmount ?? 0, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]);
|
||||
}).toList(),
|
||||
cellWidths: is58mm ? {0: null, 1: 0.2, 2: 0.25} : {0: 0.1, 1: null, 2: 0.15, 3: 0.2},
|
||||
));
|
||||
}
|
||||
|
||||
// Add Total Return Footer inside Calculation Column generally,
|
||||
// but if you want separate divider:
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/model/business_info_model.dart';
|
||||
import 'package:mobile_pos/model/sale_transaction_model.dart';
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class SaleThermalInvoiceTemplate {
|
||||
SaleThermalInvoiceTemplate({
|
||||
required this.saleInvoice,
|
||||
required this.is58mm,
|
||||
required this.business,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final SalesTransactionModel saleInvoice;
|
||||
final BusinessInformationModel business;
|
||||
final bool is58mm;
|
||||
final bool isRTL;
|
||||
final BuildContext context;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
/// Centralized Text Style to ensure Black Color everywhere
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
|
||||
String _getProductName(num detailsId) {
|
||||
final details = saleInvoice.salesDetails?.firstWhere(
|
||||
(e) => e.id == detailsId,
|
||||
orElse: () => SalesDetails(),
|
||||
);
|
||||
String name = details?.product?.productName ?? '';
|
||||
if (details?.product?.productType == 'variant' && details?.stock?.batchNo != null) {
|
||||
name += ' [${details!.stock!.batchNo}]';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
num _getProductQty(num detailsId) {
|
||||
num totalQty =
|
||||
saleInvoice.salesDetails?.firstWhere((e) => e.id == detailsId, orElse: () => SalesDetails()).quantities ?? 0;
|
||||
|
||||
// Add returned quantities back logic
|
||||
if (saleInvoice.salesReturns?.isNotEmpty ?? false) {
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
for (var det in ret.salesReturnDetails ?? []) {
|
||||
if (det.saleDetailId == detailsId) totalQty += det.returnQty ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalQty;
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
@override
|
||||
Future<List<int>> get template async {
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
// Generate Layout
|
||||
final _imageBytes = await _generateLayout();
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate invoice.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout() async {
|
||||
final _lang = lang.S.of(context);
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (business.data?.thermalInvoiceLogo != null && business.data?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + business.data!.thermalInvoiceLogo!,
|
||||
width: is58mm ? 120 : 200,
|
||||
height: is58mm ? 120 : 200,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (business.data?.invoiceScannerLogo != null && business.data?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + business.data!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Product Rows
|
||||
final productRows = _buildProductRows();
|
||||
|
||||
// 3. Prepare Return Section
|
||||
final returnWidgets = _buildReturnSection(context);
|
||||
|
||||
// 4. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
widgets: [
|
||||
// --- Header Section ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (business.data?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
business.data?.companyName ?? "N/A",
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54, isBold: false),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (saleInvoice.branch?.name != null)
|
||||
thermer.ThermerText('${_lang.branch}: ${saleInvoice.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (business.data?.meta?.showAddress == 1)
|
||||
if (business.data?.address != null || saleInvoice.branch?.address != null)
|
||||
thermer.ThermerText(
|
||||
saleInvoice.branch?.address ?? business.data?.address ?? 'N/A',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (business.data?.meta?.showPhoneNumber == 1)
|
||||
if (business.data?.phoneNumber != null || saleInvoice.branch?.phone != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${saleInvoice.branch?.phone ?? business.data?.phoneNumber ?? "N/A"}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (business.data?.vatName != null && business.data?.meta?.showVat == 1)
|
||||
thermer.ThermerText("${business.data?.vatName}: ${business.data?.vatNo}",
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.invoice,
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section (Layout adjusted based on is58mm) ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Product Table ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.price, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.amount, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: productRows,
|
||||
cellWidths: is58mm
|
||||
? {0: null, 1: 0.2, 2: 0.15, 3: 0.2} // 58mm layout
|
||||
: {0: 0.1, 1: null, 2: 0.15, 3: 0.2, 4: 0.2}, // 80mm layout
|
||||
columnSpacing: 15.0,
|
||||
rowSpacing: 3.0,
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Totals Section ---
|
||||
if (!is58mm)
|
||||
// 80mm Split Layout
|
||||
thermer.ThermerRow(
|
||||
children: [
|
||||
thermer.ThermerExpanded(flex: 4, child: thermer.ThermerAlign(child: _buildPaymentInfoText(_lang))),
|
||||
thermer.ThermerExpanded(flex: 6, child: _buildCalculationColumn(_lang)),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// 58mm Stacked Layout
|
||||
_buildCalculationColumn(_lang),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
_buildPaymentInfoText(_lang),
|
||||
],
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Returns ---
|
||||
...returnWidgets,
|
||||
|
||||
// --- Footer ---
|
||||
if (business.data?.gratitudeMessage != null && business.data?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(business.data?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (business.data?.showNote == 1)
|
||||
thermer.ThermerText('${business.data?.invoiceNoteLevel ?? _lang.note}: ${business.data?.invoiceNote}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
// if (business.data?.developByLink != null)
|
||||
// thermer.ThermerAlign(child: thermer.ThermerQRCode(data: business.data?.developByLink ?? '', size: 120)),
|
||||
|
||||
if (business.data?.developBy != null)
|
||||
thermer.ThermerText('${business.data?.developByLevel ?? _lang.developedBy} ${business.data?.developBy}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
DateTime? saleDateTime;
|
||||
|
||||
if (saleInvoice.saleDate != null && saleInvoice.saleDate!.isNotEmpty) {
|
||||
saleDateTime = DateTime.tryParse(saleInvoice.saleDate!);
|
||||
}
|
||||
|
||||
final formattedDate = saleDateTime != null
|
||||
? DateFormat('dd MMMM yyyy').format(saleDateTime) // 25 January 2026
|
||||
: '';
|
||||
|
||||
final formattedTime = saleDateTime != null
|
||||
? DateFormat('hh:mm a').format(saleDateTime).toLowerCase() // 12:55 pm
|
||||
: '';
|
||||
|
||||
final invText = '${_lang.invoice}: ${saleInvoice.invoiceNumber ?? ''}';
|
||||
final dateText = '${_lang.date}: ${formattedDate ?? ''}';
|
||||
final timeText = "${_lang.time}: ${formattedTime ?? ''}";
|
||||
final nameText = '${_lang.name}: ${saleInvoice.party?.name ?? 'Guest'}';
|
||||
final mobileText = '${_lang.mobile} ${saleInvoice.party?.phone ?? 'N/A'}';
|
||||
final salesByText =
|
||||
"${_lang.salesBy} ${saleInvoice.user?.role == 'shop-owner' ? _lang.admin : saleInvoice.user?.role ?? "N/A"}";
|
||||
|
||||
if (is58mm) {
|
||||
// 58mm: Vertical Stack (One below another)
|
||||
return [
|
||||
thermer.ThermerText(
|
||||
invText,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
if (saleInvoice.saleDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(salesByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
// 80mm: Two columns (Side by side)
|
||||
return [
|
||||
// Row 1: Invoice | Date
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (saleInvoice.saleDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
// Row 2: Name | Time
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
// Row 3: Mobile | Sales By
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(salesByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
List<thermer.ThermerTableRow> _buildProductRows() {
|
||||
List<thermer.ThermerTableRow> rows = [];
|
||||
if (saleInvoice.salesDetails == null) return rows;
|
||||
|
||||
for (var index = 0; index < saleInvoice.salesDetails!.length; index++) {
|
||||
final item = saleInvoice.salesDetails![index];
|
||||
final qty = _getProductQty(item.id ?? 0);
|
||||
final price = item.price ?? 0;
|
||||
final discount = item.discount ?? 0;
|
||||
final amount = (price * qty) - (discount * qty);
|
||||
|
||||
// Main Row
|
||||
rows.add(thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText((index + 1).toString(), style: _commonStyle()),
|
||||
thermer.ThermerText(_getProductName(item.id ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(qty), textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText('$price', textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(amount), textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]));
|
||||
|
||||
// Warranty/Guarantee
|
||||
final w = item.warrantyInfo;
|
||||
if (w?.warrantyDuration != null) {
|
||||
rows.add(_buildInfoRow("${lang.S.of(context).warranty} : ${w!.warrantyDuration} ${w.warrantyUnit}"));
|
||||
}
|
||||
if (w?.guaranteeDuration != null) {
|
||||
rows.add(_buildInfoRow("${lang.S.of(context).guarantee} : ${w!.guaranteeDuration} ${w.guaranteeUnit}"));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
thermer.ThermerTableRow _buildInfoRow(String text) {
|
||||
return thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(""),
|
||||
thermer.ThermerText(text, style: _commonStyle(fontSize: 20)),
|
||||
thermer.ThermerText(""),
|
||||
thermer.ThermerText(""),
|
||||
thermer.ThermerText(""),
|
||||
]);
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
num subTotal = 0;
|
||||
num totalDiscount = 0;
|
||||
if (saleInvoice.salesDetails != null) {
|
||||
for (var e in saleInvoice.salesDetails!) {
|
||||
final q = _getProductQty(e.id ?? 0);
|
||||
subTotal += ((e.price ?? 0) * q) - ((e.discount ?? 0) * q);
|
||||
totalDiscount += (e.discount ?? 0) * q;
|
||||
}
|
||||
}
|
||||
|
||||
num returnDiscount = 0;
|
||||
if (saleInvoice.salesReturns != null) {
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
for (var det in ret.salesReturnDetails ?? []) {
|
||||
final price = saleInvoice.salesDetails
|
||||
?.firstWhere((e) => e.id == det.saleDetailId, orElse: () => SalesDetails())
|
||||
.price ??
|
||||
0;
|
||||
returnDiscount += ((price * (det.returnQty ?? 0)) - (det.returnAmount ?? 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(
|
||||
children: [
|
||||
calcRow('${_lang.subTotal}: ', subTotal),
|
||||
calcRow('${_lang.discount}: ', (saleInvoice.discountAmount ?? 0) + returnDiscount + totalDiscount),
|
||||
calcRow("${saleInvoice.vat?.name ?? _lang.vat}: ", saleInvoice.vatAmount ?? 0, isCurrency: false),
|
||||
calcRow('${_lang.shippingCharge}:', saleInvoice.shippingCharge ?? 0, isCurrency: false),
|
||||
if ((saleInvoice.roundingAmount ?? 0) != 0) ...[
|
||||
calcRow('${_lang.total}:', saleInvoice.actualTotalAmount ?? 0),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText('${_lang.rounding}:', style: _commonStyle()),
|
||||
thermer.ThermerText(
|
||||
"${!(saleInvoice.roundingAmount?.isNegative ?? true) ? '+' : ''}${formatPointNumber(saleInvoice.roundingAmount ?? 0)}",
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
calcRow('${_lang.totalPayable}: ', saleInvoice.totalAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.paidAmount}: ',
|
||||
((saleInvoice.totalAmount ?? 0) - (saleInvoice.dueAmount ?? 0)) + (saleInvoice.changeAmount ?? 0)),
|
||||
if ((saleInvoice.dueAmount ?? 0) > 0) calcRow('${_lang.dueAmount}: ', saleInvoice.dueAmount ?? 0),
|
||||
if ((saleInvoice.changeAmount ?? 0) > 0) calcRow('${_lang.changeAmount}: ', saleInvoice.changeAmount ?? 0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
thermer.ThermerText _buildPaymentInfoText(lang.S _lang) {
|
||||
List<String> labels = [];
|
||||
if (saleInvoice.transactions != null) {
|
||||
for (var item in saleInvoice.transactions!) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = _lang.cash;
|
||||
if (item.transactionType == 'cheque_payment') label = _lang.cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = _lang.wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
return thermer.ThermerText(
|
||||
"${_lang.paidVia} : ${labels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: is58mm ? thermer.TextAlign.center : thermer.TextAlign.start,
|
||||
);
|
||||
}
|
||||
|
||||
List<thermer.ThermerWidget> _buildReturnSection(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
if (saleInvoice.salesReturns?.isEmpty ?? true) return [];
|
||||
|
||||
List<thermer.ThermerWidget> widgets = [];
|
||||
List<String> processedDates = [];
|
||||
num totalReturnedAmount = 0;
|
||||
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
final dateStr = ret.returnDate?.substring(0, 10);
|
||||
if (dateStr != null && !processedDates.contains(dateStr)) {
|
||||
processedDates.add(dateStr);
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
widgets.add(thermer.ThermerText('${_lang.retur}-$dateStr', style: _commonStyle(isBold: true)));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
thermer.ThermerText(_lang.qty,
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
thermer.ThermerText(_lang.total,
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
]),
|
||||
data: (ret.salesReturnDetails ?? []).map((d) {
|
||||
totalReturnedAmount += d.returnAmount ?? 0;
|
||||
return thermer.ThermerTableRow([
|
||||
thermer.ThermerText(_getProductName(d.saleDetailId ?? 0), style: _commonStyle(fontSize: 22)),
|
||||
thermer.ThermerText('${d.returnQty ?? 0}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(fontSize: 22)),
|
||||
thermer.ThermerText('${d.returnAmount ?? 0}',
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle(fontSize: 22)),
|
||||
]);
|
||||
}).toList(),
|
||||
cellWidths: {0: null, 1: 0.2, 2: 0.25},
|
||||
));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
widgets.add(
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(_lang.returnAmount, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(totalReturnedAmount),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
);
|
||||
widgets.add(thermer.ThermerSizedBox(height: 10));
|
||||
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
69
lib/service/thermal_print/src/templates/templates.dart
Normal file
69
lib/service/thermal_print/src/templates/templates.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// import 'dart:typed_data' show Uint8List;
|
||||
|
||||
// import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod;
|
||||
// import 'package:image/image.dart' as img;
|
||||
// import 'package:intl/intl.dart';
|
||||
// import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// import 'package:mobile_pos/generated/l10n.dart';
|
||||
// import 'package:mobile_pos/model/business_info_model.dart';
|
||||
// import 'package:mobile_pos/model/sale_transaction_model.dart';
|
||||
// import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
// // part '_purchase_invoice_template.dart';
|
||||
// part '_sale_invoice_template.dart';
|
||||
// // part '_kot_ticket_template.dart';
|
||||
// // part '_due_collection_invoice_template.dart';
|
||||
|
||||
// abstract class ThermalInvoiceTemplateBase {
|
||||
// ThermalInvoiceTemplateBase(this.ref);
|
||||
// final riverpod.Ref ref;
|
||||
|
||||
// thermer.TextDirection get textDirection {
|
||||
// final _rtlLang = ['ar', 'ar-bh', 'eg-ar', 'fa', 'prs', 'ps', 'ur'];
|
||||
|
||||
// // if (_rtlLang.contains(ref.read(GlobalContextHolder.localeProvider).languageCode)) {
|
||||
// // return thermer.TextDirection.rtl;
|
||||
// // }
|
||||
|
||||
// return thermer.TextDirection.ltr;
|
||||
// }
|
||||
|
||||
// Future<List<int>> get template;
|
||||
// // Future<img.Image?> getNetworkImage(
|
||||
// // String? url, {
|
||||
// // int width = 100,
|
||||
// // int height = 100,
|
||||
// // }) async {
|
||||
// // if (url == null) return null;
|
||||
|
||||
// // try {
|
||||
// // final _response = await dio.Dio().get<List<int>>(
|
||||
// // url,
|
||||
// // options: dio.Options(responseType: dio.ResponseType.bytes),
|
||||
// // );
|
||||
|
||||
// // final _image = img.decodeImage(Uint8List.fromList(_response.data!));
|
||||
// // if (_image == null) return null;
|
||||
|
||||
// // return img.copyResize(
|
||||
// // _image,
|
||||
// // width: width,
|
||||
// // height: height,
|
||||
// // interpolation: img.Interpolation.average,
|
||||
// // );
|
||||
// // } catch (e) {
|
||||
// // return null;
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
// // extension ThermalPrinterPaperSizeExt {
|
||||
// // PaperSize get escPosSize {
|
||||
// // return switch (this) {
|
||||
// // ThermalPrinterPaperSize.mm582Inch => PaperSize.mm58,
|
||||
// // ThermalPrinterPaperSize.mm803Inch => PaperSize.mm80,
|
||||
// // };
|
||||
// // }
|
||||
// // }
|
||||
@@ -0,0 +1,79 @@
|
||||
// import '../../../../data/repository/repository.dart';
|
||||
|
||||
// export '../../../../data/repository/repository.dart' show SalePurchaseThermalInvoiceData;
|
||||
|
||||
// final purchaseThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, SalePurchaseThermalInvoiceData>(
|
||||
// (ref, purchase) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = PurchaseThermalInvoiceTemplate(
|
||||
// ref,
|
||||
// purchaseInvoice: purchase.copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final saleThermalInvoiceProvider =
|
||||
// FutureProvider.autoDispose.family<bool, ({SalePurchaseThermalInvoiceData sale, bool printKOT})>(
|
||||
// (ref, data) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = SaleThermalInvoiceTemplate(
|
||||
// ref,
|
||||
// saleInvoice: data.sale.copyWith(user: _user),
|
||||
// printKOT: data.printKOT,
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final kotThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, Sale>(
|
||||
// (ref, sale) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = KOTTicketTemplate(
|
||||
// ref,
|
||||
// kotInvoice: SalePurchaseThermalInvoiceData.fromSale(sale).copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// final dueCollectionThermalInvoiceProvider = FutureProvider.autoDispose.family<bool, DueCollectionThermalInvoiceData>(
|
||||
// (ref, dueCollect) async {
|
||||
// final _user = ref.read(userRepositoryProvider).value;
|
||||
|
||||
// final _template = DueCollectionTemplate(
|
||||
// ref,
|
||||
// dueInvoice: dueCollect.copyWith(user: _user),
|
||||
// );
|
||||
|
||||
// return await Future.microtask(
|
||||
// () => ref.read(thermalPrinterGeneratorProvider).printInvoice(_template),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
|
||||
// sealed class InvoicePreviewType {
|
||||
// const InvoicePreviewType._();
|
||||
// }
|
||||
|
||||
// class ThermalPreview extends InvoicePreviewType {
|
||||
// final ThermalPrintInvoiceData printData;
|
||||
// final bool isSale;
|
||||
// ThermalPreview(this.printData, {this.isSale = false}) : super._();
|
||||
// }
|
||||
|
||||
// class PdfPreview extends InvoicePreviewType {
|
||||
// PdfPreview() : super._();
|
||||
// }
|
||||
322
lib/service/thermal_print/thermer/src/layouts/_layout_utils.dart
Normal file
322
lib/service/thermal_print/thermer/src/layouts/_layout_utils.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class TextMeasurementCache {
|
||||
static final Map<String, _CachedMeasurement> _cache = {};
|
||||
|
||||
static void clear() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
static _CachedMeasurement _getCachedMeasurement(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
final key = _generateKey(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
);
|
||||
if (_cache.length >= 1000) {
|
||||
_cache.clear();
|
||||
}
|
||||
return _cache[key] ??= _CachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
);
|
||||
}
|
||||
|
||||
static String _generateKey(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
return '$text|${style.hashCode}|$direction|$textAlign|$maxLines|$maxWidth|${fallbackFonts?.join(',')}';
|
||||
}
|
||||
|
||||
static double getWidth(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).width;
|
||||
}
|
||||
|
||||
static double getHeight(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).height;
|
||||
}
|
||||
|
||||
static TextPainter getPainter(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth, [
|
||||
List<String>? fallbackFonts,
|
||||
]) {
|
||||
return _getCachedMeasurement(
|
||||
text,
|
||||
style,
|
||||
direction,
|
||||
textAlign,
|
||||
maxLines,
|
||||
maxWidth,
|
||||
fallbackFonts,
|
||||
).painter;
|
||||
}
|
||||
}
|
||||
|
||||
class _CachedMeasurement {
|
||||
late final TextPainter painter;
|
||||
late final double width;
|
||||
late final double height;
|
||||
|
||||
_CachedMeasurement(
|
||||
String text,
|
||||
TextStyle style,
|
||||
TextDirection direction,
|
||||
TextAlign textAlign,
|
||||
int? maxLines,
|
||||
double maxWidth,
|
||||
List<String>? fallbackFonts,
|
||||
) {
|
||||
final effectiveStyle =
|
||||
fallbackFonts != null && fallbackFonts.isNotEmpty ? style.copyWith(fontFamilyFallback: fallbackFonts) : style;
|
||||
if (fallbackFonts != null && fallbackFonts.isNotEmpty) {
|
||||
debugPrint('TextMeasurementCache: Using font fallback for text: "$text"');
|
||||
}
|
||||
painter = TextPainter(
|
||||
text: TextSpan(text: text, style: effectiveStyle),
|
||||
textDirection: direction,
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
);
|
||||
painter.layout(maxWidth: maxWidth);
|
||||
width = painter.width;
|
||||
height = painter.height;
|
||||
}
|
||||
}
|
||||
|
||||
class LayoutUtils {
|
||||
static List<double> calculateCellWidths(
|
||||
ThermerTable table,
|
||||
double availableWidth,
|
||||
int numColumns,
|
||||
) {
|
||||
List<double> actualWidths = [];
|
||||
if (table.cellWidths != null) {
|
||||
double totalFixed = 0;
|
||||
int nullCount = 0;
|
||||
for (int col = 0; col < numColumns; col++) {
|
||||
double? frac = table.cellWidths![col];
|
||||
if (frac != null) {
|
||||
totalFixed += frac;
|
||||
} else {
|
||||
nullCount++;
|
||||
}
|
||||
}
|
||||
double remaining = 1.0 - totalFixed;
|
||||
double nullFrac = nullCount > 0 ? remaining / nullCount : 0;
|
||||
for (int col = 0; col < numColumns; col++) {
|
||||
double? frac = table.cellWidths![col];
|
||||
actualWidths.add((frac ?? nullFrac) * availableWidth);
|
||||
}
|
||||
} else {
|
||||
double totalSpacing = (numColumns - 1) * table.columnSpacing;
|
||||
double cellWidth = (availableWidth - totalSpacing) / numColumns;
|
||||
actualWidths = List.filled(numColumns, cellWidth);
|
||||
}
|
||||
return actualWidths;
|
||||
}
|
||||
|
||||
static double calculateWidgetWidth(
|
||||
ThermerWidget widget,
|
||||
double availableWidth, {
|
||||
TextDirection defaultTextDirection = TextDirection.ltr,
|
||||
}) {
|
||||
if (widget is ThermerText) {
|
||||
return TextMeasurementCache.getWidth(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? defaultTextDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
availableWidth,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
return widget.width ?? availableWidth;
|
||||
} else if (widget is ThermerQRCode) {
|
||||
return widget.size;
|
||||
} else if (widget is ThermerImage) {
|
||||
return widget.width ?? availableWidth;
|
||||
} else if (widget is ThermerExpanded) {
|
||||
return calculateWidgetWidth(widget.child, availableWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
return calculateWidgetWidth(widget.child, availableWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerAlign) {
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
static double calculateWidgetHeight(
|
||||
ThermerWidget widget,
|
||||
double maxWidth, {
|
||||
TextDirection defaultTextDirection = TextDirection.ltr,
|
||||
}) {
|
||||
if (widget is ThermerText) {
|
||||
return TextMeasurementCache.getHeight(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? defaultTextDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
maxWidth,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
} else if (widget is ThermerRow) {
|
||||
double maxHeight = 0;
|
||||
for (final child in widget.children) {
|
||||
final childHeight = calculateWidgetHeight(child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (childHeight > maxHeight) maxHeight = childHeight;
|
||||
}
|
||||
return maxHeight;
|
||||
} else if (widget is ThermerColumn) {
|
||||
double totalHeight = 0;
|
||||
for (int i = 0; i < widget.children.length; i++) {
|
||||
totalHeight += calculateWidgetHeight(widget.children[i], maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (i < widget.children.length - 1) totalHeight += widget.spacing;
|
||||
}
|
||||
return totalHeight;
|
||||
} else if (widget is ThermerTable) {
|
||||
final numColumns = widget.data.isNotEmpty ? widget.data[0].cells.length : (widget.header?.cells.length ?? 0);
|
||||
final actualWidths = calculateCellWidths(widget, maxWidth, numColumns);
|
||||
|
||||
double totalHeight = 0;
|
||||
|
||||
double borderHeight = 0;
|
||||
if (widget.enableHeaderBorders && widget.header != null) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.horizontalBorderChar,
|
||||
style: TextStyle(fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: maxWidth);
|
||||
borderHeight = textPainter.height;
|
||||
}
|
||||
|
||||
if (widget.header != null) {
|
||||
if (widget.enableHeaderBorders) {
|
||||
totalHeight += borderHeight;
|
||||
}
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.header!.cells.length; col++) {
|
||||
final cell = widget.header!.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellHeight = calculateWidgetHeight(cell, cellWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
totalHeight += rowHeight;
|
||||
if (widget.enableHeaderBorders) {
|
||||
totalHeight += widget.rowSpacing + borderHeight;
|
||||
} else {
|
||||
totalHeight += widget.rowSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < widget.data.length; i++) {
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.data[i].cells.length; col++) {
|
||||
final cell = widget.data[i].cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellHeight = calculateWidgetHeight(cell, cellWidth, defaultTextDirection: defaultTextDirection);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
totalHeight += rowHeight;
|
||||
if (i < widget.data.length - 1) totalHeight += widget.rowSpacing;
|
||||
}
|
||||
return totalHeight;
|
||||
} else if (widget is ThermerDivider) {
|
||||
if (widget.isHorizontal) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.character,
|
||||
style: TextStyle(fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: maxWidth);
|
||||
return textPainter.height;
|
||||
} else {
|
||||
return (widget.length ?? 1) * 20.0;
|
||||
}
|
||||
} else if (widget is ThermerQRCode) {
|
||||
return widget.size;
|
||||
} else if (widget is ThermerImage) {
|
||||
return widget.height ?? ((widget.width ?? maxWidth) / widget.image.width) * widget.image.height;
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
return widget.height ??
|
||||
(widget.child != null
|
||||
? calculateWidgetHeight(widget.child!, maxWidth, defaultTextDirection: defaultTextDirection)
|
||||
: 0);
|
||||
} else if (widget is ThermerExpanded) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
} else if (widget is ThermerAlign) {
|
||||
return calculateWidgetHeight(widget.child, maxWidth, defaultTextDirection: defaultTextDirection);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class LayoutItem {
|
||||
final ThermerWidget widget;
|
||||
final double height;
|
||||
|
||||
const LayoutItem({
|
||||
required this.widget,
|
||||
required this.height,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:typed_data' as type;
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import '_shared_types.dart';
|
||||
import '_thermer_painter.dart';
|
||||
import '_layout_utils.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class PaperSize {
|
||||
const PaperSize._(this._width);
|
||||
|
||||
static const mm58 = PaperSize._(58.0);
|
||||
static const mm80 = PaperSize._(80.0);
|
||||
static const mm110 = PaperSize._(110.0);
|
||||
|
||||
const PaperSize.custom(double width) : _width = width;
|
||||
|
||||
final double _width;
|
||||
|
||||
double get width => _width;
|
||||
}
|
||||
|
||||
/// Main class for creating thermal printer layouts from widgets.
|
||||
/// Handles layout calculation, rendering to image, and conversion to byte data.
|
||||
class ThermerLayout {
|
||||
/// The list of widgets to include in the layout.
|
||||
final List<ThermerWidget> widgets;
|
||||
|
||||
/// The paper size for the thermal printer.
|
||||
final PaperSize paperSize;
|
||||
|
||||
/// Dots per inch for the printer resolution.
|
||||
final double dpi;
|
||||
|
||||
/// Gap between layout items.
|
||||
final double layoutGap;
|
||||
|
||||
/// Whether to convert the output to black and white.
|
||||
final bool blackAndWhite;
|
||||
|
||||
/// Horizontal margin in millimeters to account for printer limitations.
|
||||
final double marginMm;
|
||||
|
||||
/// The default text direction for the layout.
|
||||
final TextDirection textDirection;
|
||||
|
||||
const ThermerLayout({
|
||||
required this.widgets,
|
||||
this.paperSize = PaperSize.mm80,
|
||||
double? dpi,
|
||||
this.layoutGap = 3.0,
|
||||
this.blackAndWhite = false,
|
||||
double? marginMm,
|
||||
this.textDirection = TextDirection.ltr,
|
||||
}) : dpi = dpi ?? 203.0,
|
||||
marginMm = marginMm ?? 5.0;
|
||||
|
||||
double get width => ((paperSize.width - (marginMm * 2)) / 25.4) * dpi;
|
||||
|
||||
// Process layout and calculate heights in one pass
|
||||
List<LayoutItem> _processLayout() {
|
||||
return widgets.map((widget) {
|
||||
final height = _calculateHeight(widget);
|
||||
return LayoutItem(widget: widget, height: height);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
double _calculateHeight(ThermerWidget widget) {
|
||||
// subtract margins from width to get printable area
|
||||
final printableWidth = width;
|
||||
final height = LayoutUtils.calculateWidgetHeight(widget, printableWidth, defaultTextDirection: textDirection);
|
||||
if (height == 0 && widget is ThermerText) {
|
||||
throw Exception('ThermerText height is 0 for text: "${widget.data}"');
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
double _calculateTotalHeight(List<LayoutItem> items) {
|
||||
double total = 0;
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
total += items[i].height;
|
||||
if (i < items.length - 1) total += layoutGap;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
Future<type.Uint8List> toUint8List() => generateImage();
|
||||
|
||||
Future<type.Uint8List> generateImage() async {
|
||||
TextMeasurementCache.clear();
|
||||
|
||||
if (widgets.isEmpty) {
|
||||
throw Exception('No widgets provided to ThermerLayout');
|
||||
}
|
||||
|
||||
final layoutItems = _processLayout();
|
||||
final totalHeight = _calculateTotalHeight(layoutItems);
|
||||
|
||||
if (totalHeight <= 1) {
|
||||
throw Exception('Total height is $totalHeight, cannot generate image');
|
||||
}
|
||||
|
||||
if (width <= 0 || width > 10000) {
|
||||
throw Exception('Invalid width: $width. Must be > 0 and <= 10000');
|
||||
}
|
||||
|
||||
if (totalHeight <= 0 || totalHeight > 10000) {
|
||||
throw Exception('Invalid height: $totalHeight. Must be > 0 and <= 10000');
|
||||
}
|
||||
|
||||
debugPrint('ThermerLayout: Generating image with size ${width.toInt()}x${totalHeight.toInt()}');
|
||||
|
||||
final size = ui.Size(width, totalHeight);
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
|
||||
// Draw white background
|
||||
final paint = Paint()..color = const Color(0xFFFFFFFF);
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
|
||||
|
||||
final painter = ThermerPainter(layoutItems, layoutGap: layoutGap, textDirection: textDirection);
|
||||
painter.paint(canvas, size);
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(
|
||||
size.width.toInt(),
|
||||
size.height.toInt(),
|
||||
);
|
||||
|
||||
var byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
var bytes = byteData!.buffer.asUint8List();
|
||||
|
||||
if (blackAndWhite) {
|
||||
debugPrint('ThermerLayout: Converting image to black and white');
|
||||
// Decode the PNG
|
||||
final decodedImage = img.decodePng(bytes);
|
||||
if (decodedImage != null) {
|
||||
// Convert to monochrome (1-bit)
|
||||
final monoImage = img.monochrome(decodedImage);
|
||||
// Encode back to PNG
|
||||
bytes = img.encodePng(monoImage);
|
||||
debugPrint('ThermerLayout: B&W conversion completed');
|
||||
} else {
|
||||
debugPrint('ThermerLayout: Failed to decode image for B&W conversion');
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '_shared_types.dart';
|
||||
import '_layout_utils.dart';
|
||||
import '../widgets/widgets.export.dart';
|
||||
|
||||
class ThermerPainter extends CustomPainter {
|
||||
final List<LayoutItem> layoutItems;
|
||||
final double layoutGap;
|
||||
final TextDirection textDirection;
|
||||
static const double charWidth = 10;
|
||||
|
||||
ThermerPainter(this.layoutItems,
|
||||
{this.layoutGap = 3.0, this.textDirection = TextDirection.ltr});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
double yOffset = 0;
|
||||
final linePaint = Paint()
|
||||
..color = const Color(0xFF000000)
|
||||
..strokeWidth = 1;
|
||||
|
||||
for (final item in layoutItems) {
|
||||
_paintWidget(canvas, size, item.widget, yOffset, linePaint);
|
||||
yOffset += item.height + layoutGap;
|
||||
}
|
||||
}
|
||||
|
||||
void _paintWidget(Canvas canvas, Size size, ThermerWidget widget,
|
||||
double yOffset, Paint linePaint) {
|
||||
if (widget is ThermerText) {
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
widget.data,
|
||||
widget.style ?? const TextStyle(),
|
||||
widget.direction ?? textDirection,
|
||||
widget.textAlign,
|
||||
widget.maxLines,
|
||||
size.width,
|
||||
widget.fallbackFonts,
|
||||
);
|
||||
double xOffset = 0;
|
||||
|
||||
final effectiveAlign = widget.textAlign;
|
||||
|
||||
if (effectiveAlign == TextAlign.center) {
|
||||
xOffset = (size.width - textPainter.width) / 2;
|
||||
} else if (effectiveAlign == TextAlign.right) {
|
||||
xOffset = size.width - textPainter.width;
|
||||
} else if (effectiveAlign == TextAlign.left) {
|
||||
xOffset = 0;
|
||||
} else if (effectiveAlign == TextAlign.start) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? size.width - textPainter.width
|
||||
: 0;
|
||||
} else if (effectiveAlign == TextAlign.end) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? 0
|
||||
: size.width - textPainter.width;
|
||||
} else if (effectiveAlign == TextAlign.justify) {
|
||||
xOffset = textDirection == TextDirection.rtl
|
||||
? size.width - textPainter.width
|
||||
: 0;
|
||||
}
|
||||
|
||||
textPainter.paint(canvas, Offset(xOffset, yOffset));
|
||||
} else if (widget is ThermerRow) {
|
||||
_paintRow(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerColumn) {
|
||||
_paintColumn(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerTable) {
|
||||
_paintTable(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerDivider) {
|
||||
_paintDivider(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerImage) {
|
||||
_paintImage(canvas, size, widget, yOffset);
|
||||
} else if (widget is ThermerQRCode) {
|
||||
_paintQRCode(canvas, size, widget, yOffset);
|
||||
} else if (widget is ThermerAlign) {
|
||||
_paintAlign(canvas, size, widget, yOffset, linePaint);
|
||||
} else if (widget is ThermerExpanded) {
|
||||
_paintWidget(canvas, size, widget.child, yOffset, linePaint);
|
||||
} else if (widget is ThermerFlexible) {
|
||||
_paintWidget(canvas, size, widget.child, yOffset, linePaint);
|
||||
} else if (widget is ThermerSizedBox) {
|
||||
if (widget.child != null) {
|
||||
final childSize = Size(
|
||||
widget.width ?? size.width,
|
||||
widget.height ?? _calculateChildHeight(widget.child!, size.width),
|
||||
);
|
||||
_paintWidget(canvas, childSize, widget.child!, yOffset, linePaint);
|
||||
}
|
||||
} else {
|
||||
throw Exception('Unknown widget type: ${widget.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
void _paintTable(Canvas canvas, Size size, ThermerTable widget,
|
||||
double yOffset, Paint linePaint) {
|
||||
final numColumns = widget.data.isNotEmpty
|
||||
? widget.data[0].cells.length
|
||||
: (widget.header?.cells.length ?? 0);
|
||||
final actualWidths =
|
||||
LayoutUtils.calculateCellWidths(widget, size.width, numColumns);
|
||||
|
||||
double currentY = yOffset;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
double getColumnX(int colIndex) {
|
||||
if (!isRtl) {
|
||||
double x = 0;
|
||||
for (int i = 0; i < colIndex; i++) {
|
||||
x += actualWidths[i];
|
||||
if (widget.cellWidths == null) x += widget.columnSpacing;
|
||||
}
|
||||
return x;
|
||||
} else {
|
||||
double rightEdgeOffset = 0;
|
||||
for (int i = 0; i < colIndex; i++) {
|
||||
rightEdgeOffset += actualWidths[i];
|
||||
if (widget.cellWidths == null)
|
||||
rightEdgeOffset += widget.columnSpacing;
|
||||
}
|
||||
return size.width - rightEdgeOffset - actualWidths[colIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.header != null) {
|
||||
if (widget.enableHeaderBorders) {
|
||||
_paintHorizontalBorder(
|
||||
canvas, size, currentY, widget.horizontalBorderChar);
|
||||
currentY += _getBorderHeight(size.width, widget.horizontalBorderChar);
|
||||
}
|
||||
|
||||
double rowHeight = 0;
|
||||
for (int col = 0; col < widget.header!.cells.length; col++) {
|
||||
final cellWidget = widget.header!.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellSize = Size(cellWidth, double.infinity);
|
||||
|
||||
final x = getColumnX(col);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(x, 0);
|
||||
_paintWidget(canvas, cellSize, cellWidget, currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
final cellHeight = _calculateChildHeight(cellWidget, cellWidth);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
currentY += rowHeight;
|
||||
|
||||
if (widget.enableHeaderBorders) {
|
||||
currentY += widget.rowSpacing;
|
||||
_paintHorizontalBorder(
|
||||
canvas, size, currentY, widget.horizontalBorderChar);
|
||||
currentY += _getBorderHeight(size.width, widget.horizontalBorderChar);
|
||||
}
|
||||
|
||||
currentY += widget.rowSpacing;
|
||||
}
|
||||
|
||||
for (int i = 0; i < widget.data.length; i++) {
|
||||
final row = widget.data[i];
|
||||
double rowHeight = 0;
|
||||
|
||||
for (int col = 0; col < row.cells.length; col++) {
|
||||
final cellWidget = row.cells[col];
|
||||
final cellWidth = actualWidths[col];
|
||||
final cellSize = Size(cellWidth, double.infinity);
|
||||
|
||||
final x = getColumnX(col);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(x, 0);
|
||||
_paintWidget(canvas, cellSize, cellWidget, currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
final cellHeight = _calculateChildHeight(cellWidget, cellWidth);
|
||||
if (cellHeight > rowHeight) rowHeight = cellHeight;
|
||||
}
|
||||
currentY += rowHeight;
|
||||
if (i < widget.data.length - 1) currentY += widget.rowSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
void _paintHorizontalBorder(
|
||||
Canvas canvas, Size size, double yOffset, String char) {
|
||||
const defaultStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
final borderLength =
|
||||
_calculateDividerLength(char, size.width, defaultStyle);
|
||||
final borderPainter = TextMeasurementCache.getPainter(
|
||||
char * borderLength,
|
||||
defaultStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
borderPainter.paint(canvas, Offset(0, yOffset));
|
||||
}
|
||||
|
||||
double _getBorderHeight(double width, String char) {
|
||||
const defaultStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
final borderLength = _calculateDividerLength(char, width, defaultStyle);
|
||||
final borderPainter = TextMeasurementCache.getPainter(
|
||||
char * borderLength,
|
||||
defaultStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
width,
|
||||
);
|
||||
return borderPainter.height;
|
||||
}
|
||||
|
||||
void _paintRow(Canvas canvas, Size size, ThermerRow row, double yOffset,
|
||||
Paint linePaint) {
|
||||
if (row.children.isEmpty) return;
|
||||
|
||||
final fixedChildren = <ThermerWidget>[];
|
||||
final flexibleChildren = <ThermerWidget>[];
|
||||
final fixedIndices = <int>[];
|
||||
final flexibleIndices = <int>[];
|
||||
final flexValues = <int>[];
|
||||
|
||||
for (int i = 0; i < row.children.length; i++) {
|
||||
final child = row.children[i];
|
||||
if (child is ThermerExpanded) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else if (child is ThermerFlexible &&
|
||||
child.fit == ThermerFlexFit.loose) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else {
|
||||
fixedChildren.add(child);
|
||||
fixedIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
final fixedWidths = fixedChildren
|
||||
.map((child) => _calculateChildWidth(child, size.width))
|
||||
.toList();
|
||||
|
||||
final childHeights = row.children
|
||||
.map((child) => _calculateChildHeight(
|
||||
child, _calculateChildWidth(child, size.width)))
|
||||
.toList();
|
||||
final rowHeight = childHeights.reduce((a, b) => a > b ? a : b);
|
||||
|
||||
final totalFixedWidth =
|
||||
fixedWidths.isNotEmpty ? fixedWidths.reduce((a, b) => a + b) : 0;
|
||||
final totalSpacing = (row.children.length - 1) * row.spacing;
|
||||
final remainingWidth = size.width - totalFixedWidth - totalSpacing;
|
||||
|
||||
final totalFlex =
|
||||
flexValues.isNotEmpty ? flexValues.reduce((a, b) => a + b) : 0;
|
||||
final flexibleWidths = <double>[];
|
||||
if (totalFlex > 0 && remainingWidth > 0) {
|
||||
for (final flex in flexValues) {
|
||||
flexibleWidths.add((flex / totalFlex) * remainingWidth);
|
||||
}
|
||||
} else {
|
||||
flexibleWidths.addAll(List.filled(flexibleChildren.length, 0.0));
|
||||
}
|
||||
|
||||
final actualWidths = List<double>.filled(row.children.length, 0);
|
||||
for (int i = 0; i < fixedIndices.length; i++) {
|
||||
actualWidths[fixedIndices[i]] = fixedWidths[i];
|
||||
}
|
||||
for (int i = 0; i < flexibleIndices.length; i++) {
|
||||
actualWidths[flexibleIndices[i]] = flexibleWidths[i];
|
||||
}
|
||||
|
||||
final totalChildrenWidth = actualWidths.reduce((a, b) => a + b);
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
double startX = 0;
|
||||
double dynamicSpacing = row.spacing;
|
||||
|
||||
var effectiveAlignment = row.mainAxisAlignment;
|
||||
|
||||
switch (effectiveAlignment) {
|
||||
case ThermerMainAxisAlignment.start:
|
||||
startX = isRtl ? size.width : 0;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.center:
|
||||
final offset = (size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing) /
|
||||
2;
|
||||
startX = isRtl ? size.width - offset : offset;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.end:
|
||||
final offset = size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing;
|
||||
startX = isRtl ? size.width - offset : offset;
|
||||
|
||||
startX = isRtl
|
||||
? totalChildrenWidth + (row.children.length - 1) * row.spacing
|
||||
: size.width -
|
||||
totalChildrenWidth -
|
||||
(row.children.length - 1) * row.spacing;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceBetween:
|
||||
startX = isRtl ? size.width : 0;
|
||||
if (row.children.isNotEmpty && row.children.length > 1) {
|
||||
dynamicSpacing =
|
||||
(size.width - totalChildrenWidth) / (row.children.length - 1);
|
||||
}
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceAround:
|
||||
final totalSpace = size.width - totalChildrenWidth;
|
||||
final spacePerChild =
|
||||
row.children.isNotEmpty ? totalSpace / row.children.length : 0.0;
|
||||
startX = isRtl ? size.width - spacePerChild / 2 : spacePerChild / 2;
|
||||
dynamicSpacing = spacePerChild;
|
||||
break;
|
||||
case ThermerMainAxisAlignment.spaceEvenly:
|
||||
final totalSpace = size.width - totalChildrenWidth;
|
||||
final spacePerGap = row.children.isNotEmpty
|
||||
? totalSpace / (row.children.length + 1)
|
||||
: 0.0;
|
||||
startX = isRtl ? size.width - spacePerGap : spacePerGap;
|
||||
dynamicSpacing = spacePerGap;
|
||||
break;
|
||||
}
|
||||
|
||||
double currentX = startX;
|
||||
|
||||
for (int i = 0; i < row.children.length; i++) {
|
||||
final child = row.children[i];
|
||||
final childWidth = actualWidths[i];
|
||||
final childHeight = childHeights[i];
|
||||
|
||||
double childY = yOffset;
|
||||
double effectiveChildHeight = rowHeight;
|
||||
if (row.crossAxisAlignment == ThermerCrossAxisAlignment.center) {
|
||||
childY += (rowHeight - childHeight) / 2;
|
||||
} else if (row.crossAxisAlignment == ThermerCrossAxisAlignment.end) {
|
||||
childY += rowHeight - childHeight;
|
||||
} else if (row.crossAxisAlignment == ThermerCrossAxisAlignment.stretch) {
|
||||
effectiveChildHeight = rowHeight;
|
||||
childY = yOffset;
|
||||
} else {
|
||||
effectiveChildHeight = childHeight;
|
||||
childY = yOffset;
|
||||
}
|
||||
|
||||
double paintX;
|
||||
if (isRtl) {
|
||||
currentX -= childWidth;
|
||||
paintX = currentX;
|
||||
} else {
|
||||
paintX = currentX;
|
||||
currentX += childWidth;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(paintX, 0);
|
||||
_paintWidget(canvas, Size(childWidth, effectiveChildHeight), child,
|
||||
childY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
if (i < row.children.length - 1) {
|
||||
if (isRtl) {
|
||||
currentX -= dynamicSpacing;
|
||||
} else {
|
||||
currentX += dynamicSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintColumn(Canvas canvas, Size size, ThermerColumn column,
|
||||
double yOffset, Paint linePaint) {
|
||||
if (column.children.isEmpty) return;
|
||||
|
||||
final fixedChildren = <ThermerWidget>[];
|
||||
final flexibleChildren = <ThermerWidget>[];
|
||||
final fixedIndices = <int>[];
|
||||
final flexibleIndices = <int>[];
|
||||
final flexValues = <int>[];
|
||||
|
||||
for (int i = 0; i < column.children.length; i++) {
|
||||
final child = column.children[i];
|
||||
if (child is ThermerExpanded) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else if (child is ThermerFlexible &&
|
||||
child.fit == ThermerFlexFit.loose) {
|
||||
flexibleChildren.add(child);
|
||||
flexibleIndices.add(i);
|
||||
flexValues.add(child.flex);
|
||||
} else {
|
||||
fixedChildren.add(child);
|
||||
fixedIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
final fixedHeights = fixedChildren
|
||||
.map((child) => _calculateChildHeight(
|
||||
child, _calculateChildWidth(child, size.width)))
|
||||
.toList();
|
||||
final totalFixedHeight =
|
||||
fixedHeights.isNotEmpty ? fixedHeights.reduce((a, b) => a + b) : 0;
|
||||
final totalSpacing = (column.children.length - 1) * column.spacing;
|
||||
final remainingHeight = size.height - totalFixedHeight - totalSpacing;
|
||||
|
||||
final totalFlex =
|
||||
flexValues.isNotEmpty ? flexValues.reduce((a, b) => a + b) : 0;
|
||||
final flexibleHeights = <double>[];
|
||||
if (totalFlex > 0 && remainingHeight > 0) {
|
||||
for (final flex in flexValues) {
|
||||
flexibleHeights.add((flex / totalFlex) * remainingHeight);
|
||||
}
|
||||
} else {
|
||||
flexibleHeights.addAll(List.filled(flexibleChildren.length, 0.0));
|
||||
}
|
||||
|
||||
final actualHeights = List<double>.filled(column.children.length, 0);
|
||||
for (int i = 0; i < fixedIndices.length; i++) {
|
||||
actualHeights[fixedIndices[i]] = fixedHeights[i];
|
||||
}
|
||||
for (int i = 0; i < flexibleIndices.length; i++) {
|
||||
actualHeights[flexibleIndices[i]] = flexibleHeights[i];
|
||||
}
|
||||
|
||||
double currentY = yOffset;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
for (int i = 0; i < column.children.length; i++) {
|
||||
final child = column.children[i];
|
||||
final childHeight = actualHeights[i];
|
||||
final childWidth = _calculateChildWidth(child, size.width);
|
||||
|
||||
double childX = 0;
|
||||
double effectiveChildWidth = childWidth;
|
||||
|
||||
if (column.crossAxisAlignment == ThermerCrossAxisAlignment.center) {
|
||||
childX = (size.width - effectiveChildWidth) / 2;
|
||||
} else if (column.crossAxisAlignment == ThermerCrossAxisAlignment.end) {
|
||||
childX = isRtl ? 0 : size.width - effectiveChildWidth;
|
||||
} else if (column.crossAxisAlignment == ThermerCrossAxisAlignment.start) {
|
||||
childX = isRtl ? size.width - effectiveChildWidth : 0;
|
||||
} else if (column.crossAxisAlignment ==
|
||||
ThermerCrossAxisAlignment.stretch) {
|
||||
effectiveChildWidth = size.width;
|
||||
childX = 0;
|
||||
} else {
|
||||
childX = (size.width - effectiveChildWidth) / 2;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(childX, 0);
|
||||
_paintWidget(canvas, Size(effectiveChildWidth, childHeight), child,
|
||||
currentY, linePaint);
|
||||
canvas.restore();
|
||||
|
||||
currentY += childHeight + column.spacing;
|
||||
}
|
||||
}
|
||||
|
||||
double _calculateChildWidth(ThermerWidget child, double availableWidth) {
|
||||
return LayoutUtils.calculateWidgetWidth(child, availableWidth,
|
||||
defaultTextDirection: textDirection);
|
||||
}
|
||||
|
||||
double _calculateChildHeight(ThermerWidget child, double maxWidth) {
|
||||
return LayoutUtils.calculateWidgetHeight(child, maxWidth,
|
||||
defaultTextDirection: textDirection);
|
||||
}
|
||||
|
||||
void _paintDivider(Canvas canvas, Size size, ThermerDivider divider,
|
||||
double yOffset, Paint linePaint) {
|
||||
const dividerStyle = TextStyle(
|
||||
fontSize: 20, color: Color(0xFF000000), fontWeight: FontWeight.w500);
|
||||
|
||||
if (divider.isHorizontal) {
|
||||
final length = divider.length ??
|
||||
_calculateDividerLength(divider.character, size.width, dividerStyle);
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
divider.character * length,
|
||||
dividerStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
textPainter.paint(canvas, Offset(0, yOffset));
|
||||
} else {
|
||||
final length = divider.length ?? 1;
|
||||
final textPainter = TextMeasurementCache.getPainter(
|
||||
divider.character * length,
|
||||
dividerStyle,
|
||||
TextDirection.ltr,
|
||||
TextAlign.left,
|
||||
null,
|
||||
size.width,
|
||||
);
|
||||
for (int i = 0; i < length; i++) {
|
||||
textPainter.paint(canvas, Offset(0, yOffset + i * textPainter.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateDividerLength(
|
||||
String character, double maxWidth, TextStyle style) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: character,
|
||||
style: style,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
if (textPainter.width == 0) return 0;
|
||||
return (maxWidth / textPainter.width).floor();
|
||||
}
|
||||
|
||||
void _paintImage(
|
||||
Canvas canvas, Size size, ThermerImage imageWidget, double yOffset) {
|
||||
final srcRect = Rect.fromLTWH(0, 0, imageWidget.image.width.toDouble(),
|
||||
imageWidget.image.height.toDouble());
|
||||
final dstRect = Rect.fromLTWH(0, yOffset, size.width, size.height);
|
||||
canvas.drawImageRect(imageWidget.image, srcRect, dstRect, Paint());
|
||||
}
|
||||
|
||||
void _paintQRCode(
|
||||
Canvas canvas, Size size, ThermerQRCode qrWidget, double yOffset) {
|
||||
canvas.save();
|
||||
canvas.translate(0, yOffset);
|
||||
final qrPainter = QrPainter(
|
||||
data: qrWidget.data,
|
||||
version: QrVersions.auto,
|
||||
errorCorrectionLevel: qrWidget.errorCorrectionLevel,
|
||||
dataModuleStyle: const QrDataModuleStyle(
|
||||
color: Color(0xFF000000),
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
),
|
||||
eyeStyle: const QrEyeStyle(
|
||||
color: Color(0xFF000000),
|
||||
eyeShape: QrEyeShape.square,
|
||||
),
|
||||
);
|
||||
qrPainter.paint(canvas, Size(qrWidget.size, qrWidget.size));
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void _paintAlign(Canvas canvas, Size size, ThermerAlign alignWidget,
|
||||
double yOffset, Paint linePaint) {
|
||||
final childWidth = LayoutUtils.calculateWidgetWidth(
|
||||
alignWidget.child,
|
||||
size.width,
|
||||
defaultTextDirection: textDirection,
|
||||
);
|
||||
final childHeight = LayoutUtils.calculateWidgetHeight(
|
||||
alignWidget.child,
|
||||
size.width,
|
||||
defaultTextDirection: textDirection,
|
||||
);
|
||||
|
||||
double xOffset = 0;
|
||||
final isRtl = textDirection == TextDirection.rtl;
|
||||
|
||||
switch (alignWidget.alignment) {
|
||||
case ThermerAlignment.left:
|
||||
xOffset = isRtl ? size.width - childWidth : 0;
|
||||
break;
|
||||
case ThermerAlignment.center:
|
||||
xOffset = (size.width - childWidth) / 2;
|
||||
break;
|
||||
case ThermerAlignment.right:
|
||||
xOffset = isRtl ? 0 : size.width - childWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(xOffset, 0);
|
||||
_paintWidget(canvas, Size(childWidth, childHeight), alignWidget.child,
|
||||
yOffset, linePaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export '_layout_utils.dart';
|
||||
export '_thermer_layout.dart';
|
||||
export '_thermer_painter.dart';
|
||||
12
lib/service/thermal_print/thermer/src/widgets/_align.dart
Normal file
12
lib/service/thermal_print/thermer/src/widgets/_align.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerAlign extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final ThermerAlignment alignment;
|
||||
|
||||
const ThermerAlign({
|
||||
required this.child,
|
||||
this.alignment = ThermerAlignment.center,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
abstract class ThermerWidget {
|
||||
const ThermerWidget();
|
||||
}
|
||||
19
lib/service/thermal_print/thermer/src/widgets/_column.dart
Normal file
19
lib/service/thermal_print/thermer/src/widgets/_column.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerColumn extends ThermerWidget {
|
||||
final List<ThermerWidget> children;
|
||||
|
||||
final ThermerMainAxisAlignment mainAxisAlignment;
|
||||
|
||||
final ThermerCrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
final double spacing;
|
||||
|
||||
const ThermerColumn({
|
||||
required this.children,
|
||||
this.mainAxisAlignment = ThermerMainAxisAlignment.start,
|
||||
this.crossAxisAlignment = ThermerCrossAxisAlignment.start,
|
||||
this.spacing = 3,
|
||||
});
|
||||
}
|
||||
37
lib/service/thermal_print/thermer/src/widgets/_divider.dart
Normal file
37
lib/service/thermal_print/thermer/src/widgets/_divider.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerDivider extends ThermerWidget {
|
||||
final bool isHorizontal;
|
||||
final String character;
|
||||
final int? length;
|
||||
|
||||
ThermerDivider._({
|
||||
required this.isHorizontal,
|
||||
required this.character,
|
||||
required this.length,
|
||||
});
|
||||
|
||||
ThermerDivider copyWith({String? character, int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: isHorizontal,
|
||||
character: character ?? this.character,
|
||||
length: length ?? this.length,
|
||||
);
|
||||
}
|
||||
|
||||
factory ThermerDivider.horizontal({String character = '-', int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: true,
|
||||
character: character,
|
||||
length: length,
|
||||
);
|
||||
}
|
||||
|
||||
factory ThermerDivider.vertical({String character = '|', int? length}) {
|
||||
return ThermerDivider._(
|
||||
isHorizontal: false,
|
||||
character: character,
|
||||
length: length ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
enum ThermerMainAxisAlignment { start, center, end, spaceBetween, spaceAround, spaceEvenly }
|
||||
|
||||
enum ThermerCrossAxisAlignment { start, center, end, stretch }
|
||||
|
||||
enum ThermerAlignment { left, center, right }
|
||||
11
lib/service/thermal_print/thermer/src/widgets/_expanded.dart
Normal file
11
lib/service/thermal_print/thermer/src/widgets/_expanded.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerExpanded extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final int flex;
|
||||
|
||||
const ThermerExpanded({
|
||||
required this.child,
|
||||
this.flex = 1,
|
||||
}) : assert(flex > 0, 'flex must be greater than 0');
|
||||
}
|
||||
15
lib/service/thermal_print/thermer/src/widgets/_flexible.dart
Normal file
15
lib/service/thermal_print/thermer/src/widgets/_flexible.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
enum ThermerFlexFit { tight, loose }
|
||||
|
||||
class ThermerFlexible extends ThermerWidget {
|
||||
final ThermerWidget child;
|
||||
final int flex;
|
||||
final ThermerFlexFit fit;
|
||||
|
||||
const ThermerFlexible({
|
||||
required this.child,
|
||||
this.flex = 1,
|
||||
this.fit = ThermerFlexFit.loose,
|
||||
}) : assert(flex > 0, 'flex must be greater than 0');
|
||||
}
|
||||
63
lib/service/thermal_print/thermer/src/widgets/_image.dart
Normal file
63
lib/service/thermal_print/thermer/src/widgets/_image.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image/image.dart' as img;
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerImage extends ThermerWidget {
|
||||
final ui.Image image;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const ThermerImage({
|
||||
required this.image,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
static Future<ui.Image> _convertImageToUiImage(img.Image image) async {
|
||||
final pngBytes = img.encodePng(image);
|
||||
final codec = await ui.instantiateImageCodec(pngBytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
return frame.image;
|
||||
}
|
||||
|
||||
static Future<ThermerImage> network(
|
||||
String url, {
|
||||
double? width,
|
||||
double? height,
|
||||
}) async {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to load image from $url');
|
||||
}
|
||||
final image = img.decodeImage(response.bodyBytes);
|
||||
if (image == null) {
|
||||
throw Exception('Failed to decode image from $url');
|
||||
}
|
||||
final uiImage = await _convertImageToUiImage(image);
|
||||
return ThermerImage(
|
||||
image: uiImage,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ThermerImage> memory(
|
||||
Uint8List bytes, {
|
||||
double? width,
|
||||
double? height,
|
||||
}) async {
|
||||
final image = img.decodeImage(bytes);
|
||||
if (image == null) {
|
||||
throw Exception('Failed to decode image from bytes');
|
||||
}
|
||||
final uiImage = await _convertImageToUiImage(image);
|
||||
return ThermerImage(
|
||||
image: uiImage,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
15
lib/service/thermal_print/thermer/src/widgets/_qr_code.dart
Normal file
15
lib/service/thermal_print/thermer/src/widgets/_qr_code.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerQRCode extends ThermerWidget {
|
||||
final String data;
|
||||
final double size;
|
||||
final int errorCorrectionLevel;
|
||||
|
||||
const ThermerQRCode({
|
||||
required this.data,
|
||||
this.size = 100.0,
|
||||
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||
});
|
||||
}
|
||||
19
lib/service/thermal_print/thermer/src/widgets/_row.dart
Normal file
19
lib/service/thermal_print/thermer/src/widgets/_row.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import '_base_widget.dart';
|
||||
import '_enums.dart';
|
||||
|
||||
class ThermerRow extends ThermerWidget {
|
||||
final List<ThermerWidget> children;
|
||||
|
||||
final ThermerMainAxisAlignment mainAxisAlignment;
|
||||
|
||||
final ThermerCrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
final double spacing;
|
||||
|
||||
const ThermerRow({
|
||||
required this.children,
|
||||
this.mainAxisAlignment = ThermerMainAxisAlignment.start,
|
||||
this.crossAxisAlignment = ThermerCrossAxisAlignment.center,
|
||||
this.spacing = 0,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerSizedBox extends ThermerWidget {
|
||||
final double? width;
|
||||
|
||||
final double? height;
|
||||
|
||||
final ThermerWidget? child;
|
||||
|
||||
const ThermerSizedBox({this.width, this.height, this.child});
|
||||
|
||||
const ThermerSizedBox.square({
|
||||
double dimension = 0,
|
||||
this.child,
|
||||
}) : width = dimension,
|
||||
height = dimension;
|
||||
}
|
||||
46
lib/service/thermal_print/thermer/src/widgets/_table.dart
Normal file
46
lib/service/thermal_print/thermer/src/widgets/_table.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerTableRow {
|
||||
final List<ThermerWidget> cells;
|
||||
|
||||
const ThermerTableRow(this.cells);
|
||||
}
|
||||
|
||||
class ThermerTable extends ThermerWidget {
|
||||
final List<ThermerTableRow> data;
|
||||
|
||||
final ThermerTableRow? header;
|
||||
|
||||
final Map<int, double?>? cellWidths;
|
||||
|
||||
final double columnSpacing;
|
||||
|
||||
final double rowSpacing;
|
||||
|
||||
final TextStyle? style;
|
||||
|
||||
final TextStyle? headerStyle;
|
||||
|
||||
final String horizontalBorderChar;
|
||||
|
||||
final String verticalBorderChar;
|
||||
|
||||
final bool enableHeaderBorders;
|
||||
|
||||
final bool enableTableBorders;
|
||||
|
||||
const ThermerTable({
|
||||
required this.data,
|
||||
this.header,
|
||||
this.cellWidths,
|
||||
this.columnSpacing = 10.0,
|
||||
this.rowSpacing = 3.0,
|
||||
this.style,
|
||||
this.headerStyle,
|
||||
this.horizontalBorderChar = '-',
|
||||
this.verticalBorderChar = '|',
|
||||
this.enableHeaderBorders = true,
|
||||
this.enableTableBorders = false,
|
||||
});
|
||||
}
|
||||
26
lib/service/thermal_print/thermer/src/widgets/_text.dart
Normal file
26
lib/service/thermal_print/thermer/src/widgets/_text.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '_base_widget.dart';
|
||||
|
||||
class ThermerText extends ThermerWidget {
|
||||
ThermerText(
|
||||
this.data, {
|
||||
this.direction,
|
||||
this.style = const TextStyle(color: Color(0xFF000000), fontWeight: FontWeight.w500),
|
||||
this.textAlign = TextAlign.left,
|
||||
this.maxLines,
|
||||
this.fallbackFonts,
|
||||
});
|
||||
|
||||
final String data;
|
||||
|
||||
final TextDirection? direction;
|
||||
|
||||
final TextStyle? style;
|
||||
|
||||
final TextAlign textAlign;
|
||||
|
||||
final int? maxLines;
|
||||
|
||||
final List<String>? fallbackFonts;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export '_align.dart';
|
||||
export '_base_widget.dart';
|
||||
export '_column.dart';
|
||||
export '_divider.dart';
|
||||
export '_enums.dart';
|
||||
export '_expanded.dart';
|
||||
export '_flexible.dart';
|
||||
export '_image.dart';
|
||||
export '_qr_code.dart';
|
||||
export '_row.dart';
|
||||
export '_sized_box.dart';
|
||||
export '_table.dart';
|
||||
export '_text.dart';
|
||||
6
lib/service/thermal_print/thermer/thermer.dart
Normal file
6
lib/service/thermal_print/thermer/thermer.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
library;
|
||||
|
||||
export 'src/layouts/layouts.export.dart';
|
||||
export 'src/widgets/widgets.export.dart';
|
||||
export 'package:flutter/material.dart'
|
||||
show TextStyle, Color, Colors, TextAlign, FontWeight, TextDecoration, TextDirection;
|
||||
Reference in New Issue
Block a user