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

@@ -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 '../../../../core/di/providers.dart';
import '../../../../core/services/print_service.dart';
import '../../../users/domain/entities/user_entity.dart';
import '../../data/models/create_product_warehouse_request.dart';
import '../../domain/entities/product_stage_entity.dart';
@@ -118,21 +119,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
operationTitle,
style: textTheme.titleMedium,
),
Text(
productName,
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
title: Text('${operationTitle} ${productName}'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
@@ -149,6 +136,11 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
selectedIndex: selectedIndex,
theme: theme,
),
bottomNavigationBar: _buildBottomActionBar(
selectedStage: selectedStage,
stages: stages,
theme: theme,
),
);
}
@@ -277,7 +269,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (displayStages.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
border: Border(
@@ -289,39 +281,6 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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(
spacing: 8,
runSpacing: 8,
@@ -376,43 +335,24 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
spacing: 8,
children: [
// Stage header
_buildStageHeader(stageToShow, theme),
_buildSectionCard(
theme: theme,
title: 'Thông tin công đoạn',
icon: Icons.info_outlined,
children: [
_buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'),
if (stageToShow.productStageId != null)
_buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'),
if (stageToShow.actionTypeId != null)
_buildInfoRow('Mã loại thao tác', '${stageToShow.actionTypeId}'),
_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',
),
],
),
// _buildStageHeader(stageToShow, theme),
//
// _buildSectionCard(
// theme: theme,
// title: 'Thông tin công đoạn',
// icon: Icons.info_outlined,
// children: [
// _buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'),
// if (stageToShow.productStageId != null)
// _buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'),
// if (stageToShow.actionTypeId != null)
// _buildInfoRow('Mã loại thao tác', '${stageToShow.actionTypeId}'),
// _buildInfoRow('Tên công đoạn', stageToShow.displayName),
// ],
// ),
// Add New Quantities section
_buildSectionCard(
@@ -462,6 +402,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
},
theme: theme,
),
const SizedBox(height: 8),
// All Employees Dropdown
_buildUserDropdown(
label: 'Nhân viên',
@@ -476,30 +417,21 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
),
]),
// Action buttons
Row(
spacing: 12,
// Current Quantity information
_buildSectionCard(
theme: theme,
title: 'Số lượng hiện tại',
icon: Icons.info_outlined,
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),
),
),
_buildInfoRow('Số lượng đạt', '${stageToShow.passedQuantity}'),
_buildInfoRow(
'Khối lượng đạt',
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
),
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,14 +445,55 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
);
}
void _printQuantities(ProductStageEntity stage) {
// TODO: Implement print functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng in đang phát triển'),
duration: Duration(seconds: 2),
),
);
Future<void> _printQuantities(ProductStageEntity stage) async {
// 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(
SnackBar(
content: Text('Error printing: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
Future<void> _addNewQuantities(ProductStageEntity stage) async {
@@ -708,28 +681,11 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
spacing: 4,
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,
],
),
@@ -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),
),
),
),
],
),
),
),
);
}
}