This commit is contained in:
Phuoc Nguyen
2025-10-29 15:52:24 +07:00
parent 2905668358
commit cb4df363ab
6 changed files with 707 additions and 137 deletions

View File

@@ -8,6 +8,8 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- printing (1.0.0):
- Flutter
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -17,6 +19,7 @@ DEPENDENCIES:
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- printing (from `.symlinks/plugins/printing/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
@@ -28,15 +31,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/mobile_scanner/darwin" :path: ".symlinks/plugins/mobile_scanner/darwin"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
printing:
:path: ".symlinks/plugins/printing/ios"
sqflite_darwin: sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -0,0 +1,368 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
/// Service for generating and printing warehouse export forms
class PrintService {
/// Generate and print a warehouse export form
static Future<void> printWarehouseExport({
required BuildContext context,
required String warehouseName,
required int productId,
required String productCode,
required String productName,
String? stageName,
required double passedKg,
required int passedPcs,
required double issuedKg,
required int issuedPcs,
String? responsibleName,
String? barcodeData,
}) async {
final pdf = pw.Document();
// Format current date
final dt = DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
// Add page to PDF
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(12),
build: (pw.Context pdfContext) {
return pw.Container(
padding: const pw.EdgeInsets.all(12),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Title
pw.Center(
child: pw.Column(
children: [
pw.Text(
'PHIẾU XUẤT KHO',
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 8),
pw.Text(
'Công ty TNHH Cơ Khí Chính Xác Minh Thư',
style: const pw.TextStyle(fontSize: 16),
),
pw.SizedBox(height: 4),
pw.Text(
warehouseName,
style: const pw.TextStyle(fontSize: 14),
),
pw.SizedBox(height: 4),
pw.Text(
'Ngày: $dt',
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey700,
),
),
],
),
),
pw.SizedBox(height: 16),
// Product information box
pw.Container(
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.5),
borderRadius: pw.BorderRadius.circular(8),
),
padding: const pw.EdgeInsets.all(8),
child: pw.Column(
children: [
pw.Row(
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'ProductId',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
'$productId',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Mã sản phẩm',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
productCode,
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Tên sản phẩm',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
productName,
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Công đoạn',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
stageName ?? '-',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
],
),
],
),
),
pw.SizedBox(height: 12),
// Quantities box
pw.Container(
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.5),
borderRadius: pw.BorderRadius.circular(8),
),
padding: const pw.EdgeInsets.all(8),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Số lượng:',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 6),
pw.Table(
border: pw.TableBorder.all(
color: PdfColors.black,
width: 0.5,
),
children: [
// Header
pw.TableRow(
decoration: const pw.BoxDecoration(
color: PdfColors.grey300,
),
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(
'Loại',
style: pw.TextStyle(
fontWeight: pw.FontWeight.bold,
),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(
'KG',
style: pw.TextStyle(
fontWeight: pw.FontWeight.bold,
),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(
'PCS',
style: pw.TextStyle(
fontWeight: pw.FontWeight.bold,
),
),
),
],
),
// Passed quantity row
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text('Hàng đạt'),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(passedKg.toStringAsFixed(2)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text('$passedPcs'),
),
],
),
// Issued quantity row
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text('Hàng lỗi'),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text(issuedKg.toStringAsFixed(2)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: pw.Text('$issuedPcs'),
),
],
),
],
),
],
),
),
pw.SizedBox(height: 12),
// Responsible person box
pw.Container(
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.5),
borderRadius: pw.BorderRadius.circular(8),
),
padding: const pw.EdgeInsets.all(8),
child: pw.Row(
children: [
pw.Text(
'Nhân viên kho: ',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.Text(
responsibleName ?? '-',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
pw.SizedBox(height: 12),
// Barcode section
if (barcodeData != null && barcodeData.isNotEmpty)
pw.Center(
child: pw.BarcodeWidget(
barcode: pw.Barcode.code128(),
data: barcodeData,
width: 200,
height: 60,
),
),
pw.Spacer(),
// Footer signature section
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
pw.Container(
width: 150,
child: pw.Column(
children: [
pw.Text(
'Người nhận',
style: pw.TextStyle(
fontSize: 10,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 40),
pw.Container(
height: 1,
color: PdfColors.grey700,
),
],
),
),
],
),
],
),
);
},
),
);
// Show print preview dialog
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => pdf.save(),
name: 'warehouse_export_${productCode}_${DateTime.now().millisecondsSinceEpoch}.pdf',
);
}
}

106
lib/docs/import.html Normal file
View File

@@ -0,0 +1,106 @@
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8"/>
<title>Phiếu xuất kho</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style>
:root { --fg:#111; --muted:#666; --border:#000; --primary:#2563eb; }
html,body { margin:0; padding:0; font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif; color:var(--fg); }
.wrap { max-width:720px; margin:0 auto; padding:12px; }
.actions { position: sticky; top:0; background:#fff; padding:8px 0; display:flex; gap:8px; justify-content:flex-end; border-bottom:0.1mm solid var(--border); }
.actions button { padding:6px 12px; cursor:pointer; border:0.1mm solid var(--border); background:#fff; border-radius:6px; }
h1 { font-size:20px; margin:8px 0 4px; text-align:center; }
.meta { text-align:center; color:var(--muted); margin-bottom:8px; }
.box { border:0.1mm solid var(--border); border-radius:8px; padding:8px; margin:8px 0; }
.row { display:flex; gap:8px; margin:6px 0; }
.row > div { flex:1; }
.label { color:var(--muted); font-size:12px; }
.value { font-weight:600; }
table { width:100%; border-collapse:collapse; margin-top:6px; }
th, td { border:0.1mm solid var(--border); padding:8px; text-align:left; }
th { background:#f8fafc; }
.barcode { text-align:center; margin:12px 0; }
.footer { display:flex; gap:12px; margin:8px 0 4px; }
.sign { flex:1; text-align:center; color:var(--muted); padding-top:24px; }
/* Ensure printer keeps border colors/thickness */
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* Print margins and padding */
@page {
size: auto;
margin: 3mm 0mm; /* outer page margin */
}
@media print {
.actions { display:none; }
.wrap { padding:0 4px ; }
th { background:#eee; } /* light gray still visible on most printers */
/* Force black borders on print */
.box, table, th, td { border-color:#000 !important; border-width:0.1mm !important; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="actions">
<button onclick="printAndClose()">In</button>
<button onclick="window.close()">Đóng</button>
</div>
<h1>PHIẾU XUẤT KHO</h1>
<h3>Công ty TNHH Cơ Khí Chính Xác Minh Thư</h3>
<h4>${wareHouseText}</h4>
<div class="meta">Ngày: ${dt}</div>
<div class="box">
<div class="row">
<div><div class="label">ProductId</div><div class="value">${productId}</div></div>
<div><div class="label">Mã sản phẩm</div><div class="value">${productCode}</div></div>
</div>
<div class="row">
<div><div class="label">Tên sản phẩm</div><div class="value">${productName}</div></div>
<div><div class="label">Công đoạn</div><div class="value">${stageName || '-'}</div></div>
</div>
</div>
<div class="box">
<div class="label">Số lượng:</div>
<table>
<thead>
<tr><th>Loại</th><th>KG</th><th>PCS</th></tr>
</thead>
<tbody>
<tr>
<td>Hàng đạt</td>
<td>${Number(qty.passedKg || 0)}</td>
<td>${Number(qty.passedPcs || 0)}</td>
</tr>
<tr>
<td>Hàng lỗi</td>
<td>${Number(qty.issuedKg || 0)}</td>
<td>${Number(qty.issuedPcs || 0)}</td>
</tr>
</tbody>
</table>
</div>
<div class="box">
<div class="row">
<div><div class="label">Nhân viên kho</div><div class="value">${responsibleName || '-'}</div></div>
</div>
</div>
<div class="barcode">
${barcodeDataUrl ? `<img alt="Barcode" src="${barcodeDataUrl}" />` : ''}
</div>
<div class="footer">
<div class="sign">
.
</div>
</div>
</div>
<script>
let printed = false;
function printAndClose() { printed = true; window.print(); }
window.addEventListener('afterprint', () => setTimeout(() => window.close(), 200));
window.addEventListener('focus', () => { if (printed) setTimeout(() => window.close(), 400); });
window.onload = () => { printAndClose(); };
</script>
</body>
</html>

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart'; import '../../../../core/di/providers.dart';
import '../../../../core/services/print_service.dart';
import '../../../users/domain/entities/user_entity.dart'; import '../../../users/domain/entities/user_entity.dart';
import '../../data/models/create_product_warehouse_request.dart'; import '../../data/models/create_product_warehouse_request.dart';
import '../../domain/entities/product_stage_entity.dart'; import '../../domain/entities/product_stage_entity.dart';
@@ -118,21 +119,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Column( title: Text('${operationTitle} ${productName}'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
operationTitle,
style: textTheme.titleMedium,
),
Text(
productName,
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
@@ -149,6 +136,11 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
selectedIndex: selectedIndex, selectedIndex: selectedIndex,
theme: theme, theme: theme,
), ),
bottomNavigationBar: _buildBottomActionBar(
selectedStage: selectedStage,
stages: stages,
theme: theme,
),
); );
} }
@@ -277,7 +269,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (displayStages.isNotEmpty) if (displayStages.isNotEmpty)
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: theme.colorScheme.surfaceContainerHighest,
border: Border( border: Border(
@@ -289,39 +281,6 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(
children: [
Text(
widget.stageId != null
? 'Công đoạn'
: 'Công đoạn (${displayStages.length})',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (widget.stageId != null) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'ID: ${widget.stageId}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
const SizedBox(height: 12),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
@@ -376,43 +335,24 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16, spacing: 8,
children: [ children: [
// Stage header // Stage header
_buildStageHeader(stageToShow, theme), // _buildStageHeader(stageToShow, theme),
//
_buildSectionCard( // _buildSectionCard(
theme: theme, // theme: theme,
title: 'Thông tin công đoạn', // title: 'Thông tin công đoạn',
icon: Icons.info_outlined, // icon: Icons.info_outlined,
children: [ // children: [
_buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'), // _buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'),
if (stageToShow.productStageId != null) // if (stageToShow.productStageId != null)
_buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'), // _buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'),
if (stageToShow.actionTypeId != null) // if (stageToShow.actionTypeId != null)
_buildInfoRow('Mã loại thao tác', '${stageToShow.actionTypeId}'), // _buildInfoRow('Mã loại thao tác', '${stageToShow.actionTypeId}'),
_buildInfoRow('Tên công đoạn', stageToShow.displayName), // _buildInfoRow('Tên công đoạn', stageToShow.displayName),
], // ],
), // ),
// Current Quantity information
_buildSectionCard(
theme: theme,
title: 'Số lượng hiện tại',
icon: Icons.info_outlined,
children: [
_buildInfoRow('Số lượng đạt', '${stageToShow.passedQuantity}'),
_buildInfoRow(
'Khối lượng đạt',
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
),
_buildInfoRow('Số lượng lỗi', '${stageToShow.issuedQuantity}'),
_buildInfoRow(
'Khối lượng lỗi',
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
),
],
),
// Add New Quantities section // Add New Quantities section
_buildSectionCard( _buildSectionCard(
@@ -462,6 +402,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
}, },
theme: theme, theme: theme,
), ),
const SizedBox(height: 8),
// All Employees Dropdown // All Employees Dropdown
_buildUserDropdown( _buildUserDropdown(
label: 'Nhân viên', label: 'Nhân viên',
@@ -476,30 +417,21 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
), ),
]), ]),
// Action buttons // Current Quantity information
Row( _buildSectionCard(
spacing: 12, theme: theme,
title: 'Số lượng hiện tại',
icon: Icons.info_outlined,
children: [ children: [
_buildInfoRow('Số lượng đạt', '${stageToShow.passedQuantity}'),
Expanded( _buildInfoRow(
child: OutlinedButton.icon( 'Khối lượng đạt',
onPressed: () => _printQuantities(stageToShow), '${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
icon: const Icon(Icons.print),
label: const Text('In'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
Expanded(
child: FilledButton.icon(
onPressed: () => _addNewQuantities(stageToShow),
icon: const Icon(Icons.save),
label: const Text('Lưu'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
), ),
_buildInfoRow('Số lượng lỗi', '${stageToShow.issuedQuantity}'),
_buildInfoRow(
'Khối lượng lỗi',
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
), ),
], ],
), ),
@@ -513,15 +445,56 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
); );
} }
void _printQuantities(ProductStageEntity stage) { Future<void> _printQuantities(ProductStageEntity stage) async {
// TODO: Implement print functionality // Get the current quantity values (entered by user or use current values)
final passedQuantity = int.tryParse(_passedQuantityController.text) ?? 0;
final passedWeight = double.tryParse(_passedWeightController.text) ?? 0.0;
final issuedQuantity = int.tryParse(_issuedQuantityController.text) ?? 0;
final issuedWeight = double.tryParse(_issuedWeightController.text) ?? 0.0;
// Use entered values if available, otherwise use current stock values
final finalPassedPcs = passedQuantity > 0 ? passedQuantity : stage.passedQuantity;
final finalPassedKg = passedWeight > 0.0 ? passedWeight : stage.passedQuantityWeight;
final finalIssuedPcs = issuedQuantity > 0 ? issuedQuantity : stage.issuedQuantity;
final finalIssuedKg = issuedWeight > 0.0 ? issuedWeight : stage.issuedQuantityWeight;
// Get responsible user name
final responsibleName = _selectedWarehouseUser != null
? '${_selectedWarehouseUser!.name} ${_selectedWarehouseUser!.firstName}'
: null;
// Generate barcode data (using product code or product ID)
final barcodeData = stage.productCode.isNotEmpty
? stage.productCode
: 'P${stage.productId}';
try {
await PrintService.printWarehouseExport(
context: context,
warehouseName: widget.warehouseName,
productId: stage.productId,
productCode: stage.productCode,
productName: stage.productName,
stageName: stage.displayName,
passedKg: finalPassedKg,
passedPcs: finalPassedPcs,
issuedKg: finalIssuedKg,
issuedPcs: finalIssuedPcs,
responsibleName: responsibleName,
barcodeData: barcodeData,
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Tính năng in đang phát triển'), content: Text('Error printing: ${e.toString()}'),
duration: Duration(seconds: 2), backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
), ),
); );
} }
}
}
Future<void> _addNewQuantities(ProductStageEntity stage) async { Future<void> _addNewQuantities(ProductStageEntity stage) async {
// Parse the values from text fields // Parse the values from text fields
@@ -708,28 +681,11 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
}) { }) {
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12, spacing: 4,
children: [ children: [
Row(
children: [
Icon(
icon,
size: 20,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
...children, ...children,
], ],
), ),
@@ -921,4 +877,69 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
} }
} }
Widget? _buildBottomActionBar({
required ProductStageEntity? selectedStage,
required List<ProductStageEntity> stages,
required ThemeData theme,
}) {
// Determine which stage to show
// When stageId is provided, use the filtered stage
final displayStages = widget.stageId != null
? stages.where((stage) => stage.productStageId == widget.stageId).toList()
: stages;
final stageToShow = widget.stageId != null && displayStages.isNotEmpty
? displayStages.first
: selectedStage;
// Don't show action bar if there's no stage to work with
if (stageToShow == null) {
return null;
}
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
color: theme.colorScheme.shadow.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
spacing: 12,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _printQuantities(stageToShow),
icon: const Icon(Icons.print),
label: const Text('In'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
Expanded(
child: FilledButton.icon(
onPressed: () => _addNewQuantities(stageToShow),
icon: const Icon(Icons.save),
label: const Text('Lưu'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
),
),
);
}
} }

View File

@@ -49,6 +49,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
barcode_widget:
dependency: "direct main"
description:
name: barcode_widget
sha256: "6f2c5b08659b1a5f4d88d183e6007133ea2f96e50e7b8bb628f03266c3931427"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -664,6 +688,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -712,6 +744,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.3"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -752,6 +800,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
printing:
dependency: "direct main"
description:
name: printing
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
url: "https://pub.dev"
source: hosted
version: "5.14.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -768,6 +824,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:

View File

@@ -36,6 +36,11 @@ dependencies:
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
# Printing & PDF
printing: ^5.13.4
pdf: ^3.11.3
barcode_widget: ^2.0.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter