Compare commits
7 Commits
2495330bf5
...
sunmi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ff639fc42 | ||
|
|
1cfdd2c0c6 | ||
| ff25363a19 | |||
| 9df4b79a66 | |||
| 2a6ec8f6b8 | |||
| f47700ad2b | |||
| 68cc5c0df3 |
@@ -38,11 +38,11 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
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
|
||||||
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
|
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
|
|
||||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class ApiEndpoints {
|
|||||||
/// Response: List of users
|
/// Response: List of users
|
||||||
static const String users = '/PortalUser/GetAllMemberUserShortInfo';
|
static const String users = '/PortalUser/GetAllMemberUserShortInfo';
|
||||||
|
|
||||||
|
/// Get current logged-in user
|
||||||
|
/// GET: /PortalUser/GetCurrentUser?getDep=false (requires auth token)
|
||||||
|
/// Response: Current user details
|
||||||
|
static const String getCurrentUser = '/PortalUser/GetCurrentUser?getDep=false';
|
||||||
|
|
||||||
// ==================== Warehouse Endpoints ====================
|
// ==================== Warehouse Endpoints ====================
|
||||||
|
|
||||||
/// Get all warehouses
|
/// Get all warehouses
|
||||||
|
|||||||
210
lib/core/services/sunmi_service.dart
Normal file
210
lib/core/services/sunmi_service.dart
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:sunmi_printer_plus/sunmi_printer_plus.dart';
|
||||||
|
|
||||||
|
/// Service for printing to Sunmi thermal printers
|
||||||
|
class SunmiService {
|
||||||
|
/// Print warehouse export form to Sunmi printer
|
||||||
|
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? receiverName,
|
||||||
|
String? barcodeData,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Format current date
|
||||||
|
final dt = DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now());
|
||||||
|
|
||||||
|
// Title - PHIẾU XUẤT KHO
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'PHIEU XUAT KHO',
|
||||||
|
style: SunmiTextStyle(
|
||||||
|
align: SunmiPrintAlign.CENTER,
|
||||||
|
bold: true,
|
||||||
|
fontSize: 48,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Company name
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Cong ty TNHH Co Khi Chinh Xac Minh Thu',
|
||||||
|
style: SunmiTextStyle(
|
||||||
|
align: SunmiPrintAlign.CENTER,
|
||||||
|
fontSize: 32,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Warehouse name
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
warehouseName,
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.CENTER),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Date
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Ngay: $dt',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.CENTER),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(2);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Product information
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'THONG TIN SAN PHAM',
|
||||||
|
style: SunmiTextStyle(
|
||||||
|
align: SunmiPrintAlign.LEFT,
|
||||||
|
bold: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// ProductId
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'ProductId: $productId',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Product Code
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Ma san pham: $productCode',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Product Name
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Ten san pham: $productName',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Stage Name
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Cong doan: ${stageName ?? '-'}',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(2);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Quantities
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'SO LUONG',
|
||||||
|
style: SunmiTextStyle(
|
||||||
|
align: SunmiPrintAlign.LEFT,
|
||||||
|
bold: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Loai KG PCS',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
|
||||||
|
// Passed quantity (Hàng đạt)
|
||||||
|
final passedLine =
|
||||||
|
'Hang dat ${passedKg.toStringAsFixed(2).padLeft(7)} ${passedPcs.toString().padLeft(5)}';
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
passedLine,
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Issued quantity (Hàng lỗi)
|
||||||
|
final issuedLine =
|
||||||
|
'Hang loi ${issuedKg.toStringAsFixed(2).padLeft(7)} ${issuedPcs.toString().padLeft(5)}';
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
issuedLine,
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(2);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Responsible person
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Nhan vien kho: ${responsibleName ?? '-'}',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(1);
|
||||||
|
|
||||||
|
// Receiver
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Nhan vien tiep nhan: ${receiverName ?? '-'}',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.LEFT),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(2);
|
||||||
|
|
||||||
|
// Barcode
|
||||||
|
if (barcodeData != null && barcodeData.isNotEmpty) {
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
await SunmiPrinter.printBarCode(
|
||||||
|
barcodeData,
|
||||||
|
style: SunmiBarcodeStyle(
|
||||||
|
type: SunmiBarcodeType.CODE128,
|
||||||
|
textPos: SunmiBarcodeTextPos.TEXT_UNDER,
|
||||||
|
height: 100,
|
||||||
|
align: SunmiPrintAlign.CENTER,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
await SunmiPrinter.line();
|
||||||
|
await SunmiPrinter.printText(
|
||||||
|
'Nguoi nhan',
|
||||||
|
style: SunmiTextStyle(align: SunmiPrintAlign.CENTER),
|
||||||
|
);
|
||||||
|
await SunmiPrinter.lineWrap(4);
|
||||||
|
|
||||||
|
// Cut paper
|
||||||
|
await SunmiPrinter.cutPaper();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Đã in thành công!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Show error message
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Lỗi khi in: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,9 @@ class SecureStorage {
|
|||||||
/// Key for storing email
|
/// Key for storing email
|
||||||
static const String _emailKey = 'email';
|
static const String _emailKey = 'email';
|
||||||
|
|
||||||
|
/// Key for storing current user ID
|
||||||
|
static const String _currentUserIdKey = 'current_user_id';
|
||||||
|
|
||||||
// ==================== Token Management ====================
|
// ==================== Token Management ====================
|
||||||
|
|
||||||
/// Save access token securely
|
/// Save access token securely
|
||||||
@@ -147,6 +150,25 @@ class SecureStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save current user ID
|
||||||
|
Future<void> saveCurrentUserId(int userId) async {
|
||||||
|
try {
|
||||||
|
await _storage.write(key: _currentUserIdKey, value: userId.toString());
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to save current user ID: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current user ID
|
||||||
|
Future<int?> getCurrentUserId() async {
|
||||||
|
try {
|
||||||
|
final value = await _storage.read(key: _currentUserIdKey);
|
||||||
|
return value != null ? int.tryParse(value) : null;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to read current user ID: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if user is authenticated (has valid access token)
|
/// Check if user is authenticated (has valid access token)
|
||||||
Future<bool> isAuthenticated() async {
|
Future<bool> isAuthenticated() async {
|
||||||
final token = await getAccessToken();
|
final token = await getAccessToken();
|
||||||
|
|||||||
@@ -128,3 +128,24 @@ curl --request GET \
|
|||||||
--header 'Sec-Fetch-Mode: cors' \
|
--header 'Sec-Fetch-Mode: cors' \
|
||||||
--header 'Sec-Fetch-Site: same-site' \
|
--header 'Sec-Fetch-Site: same-site' \
|
||||||
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
|
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0'
|
||||||
|
|
||||||
|
|
||||||
|
#Get current user
|
||||||
|
curl --request GET \
|
||||||
|
--url 'https://dotnet.elidev.info:8157/ws/PortalUser/GetCurrentUser?getDep=false' \
|
||||||
|
--compressed \
|
||||||
|
--header 'Accept: application/json, text/plain, */*' \
|
||||||
|
--header 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||||
|
--header 'Accept-Language: en-US,en;q=0.5' \
|
||||||
|
--header 'AccessToken: 1k5fXyQVXtGkfjwS4g2USldGyLSA7Zwa2jdj5tLe+3YfSDlk02aYgqsFh5xArkdL4N529x7IYJOGrLJJgLBPNVgD51zFfEYBzfJmMH2RUm7iegvDJaMCLISySw0zd6kcsqeJi7vtuybgY2NDPxDgiSOj4wX417PzB8AVg5bl1ZAAJ3LcVAqqtA1PDTU5ZU1QQYapNeBNxAHjnd2ojTZK1GJBIyY5Gd8P9gB880ppAKq8manNMZYsa4d8tkYf0SJUul2aqLIWJAwDGORpPmfjqkN4hMh85xAfPTZi6m4DdI0u2rHDMLaZ8eIsV16qA8wimSDnWi0VeG0SZ4ugbCdJAi3t1/uICTftiy06PJEkulBLV+h2xS/7SlmEY2xoN5ISi++3FNqsFPGa9QH6akGu2C7IXEUBCg3iGJx0uL+vULmVqk5OJIXdqiKVQ366hvhPlK2AM1zbh49x/ngibe08483WTL5uAY/fsKuBxQCpTc2368Gqhpd7QRtZFKpzikhyTWsR3nQIi6ExSstCeFbe8ehgo0PuTPZNHH5IHTc49snH6IZrSbR+F62Wu/D+4DlvMTK/ktG6LVQ3r3jSJC5MAQDV5Q9WK3RvsWMPvZrsaVW/Exz0GBgWP4W0adADg7MFSlnGDOJm6I4fCLHZIJCUww50L6iNmzvrdibrQT5jKACVgNquMZCfeZlf3m2BwUx9T6J45lAePpJ+QaMh+2voFqRiOLi98MLqOG6TW7z96sadzFVR9YU1xwM51jQDjnUlrXt0+msq29Jqt8LoCyQsG4r3RgS/tUJhximq11MDXsSXanpYM7jesjr8mAG4qjYN6z6c1Gl5N0dhcDF4HeEaIlNIgZ75FqtXZnLqvhHPyk6L2iR2ZT15nobZxLzOUad4a0OymUDUv7xuEBdEk5kmzZLDpbOxrKiyMpGSlbBhEoBMoA0u6ZKtBGQfCJ02s6Ri0WhLLM4XJCjGrpoEkTUuZ7YG39Zva19HGV0kkxeFYkG0lnZBO6jCggem5f+S2NQvXP/kUrWX1GeQFCq5PScvwJexLsbh0LKC2MGovkecoBKtNIK21V6ztvWL8lThJAl9' \
|
||||||
|
--header 'AppID: Minhthu2016' \
|
||||||
|
--header 'Connection: keep-alive' \
|
||||||
|
--header 'Origin: https://dotnet.elidev.info:8158' \
|
||||||
|
--header 'Priority: u=0' \
|
||||||
|
--header 'Referer: https://dotnet.elidev.info:8158/' \
|
||||||
|
--header 'Sec-Fetch-Dest: empty' \
|
||||||
|
--header 'Sec-Fetch-Mode: cors' \
|
||||||
|
--header 'Sec-Fetch-Site: same-site' \
|
||||||
|
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
|
||||||
|
--header 'content-type: application/json' \
|
||||||
|
--data ''
|
||||||
@@ -102,7 +102,7 @@ class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
|
|||||||
// The API returns a list of stages for the product
|
// The API returns a list of stages for the product
|
||||||
final list = json as List;
|
final list = json as List;
|
||||||
if (list.isEmpty) {
|
if (list.isEmpty) {
|
||||||
throw const ServerException('Product stages not found');
|
throw const ServerException('Không tìm thấy sản phẩm');
|
||||||
}
|
}
|
||||||
// Parse all stages from the list
|
// Parse all stages from the list
|
||||||
return list
|
return list
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:dropdown_search/dropdown_search.dart';
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
import '../../../../core/di/providers.dart';
|
import '../../../../core/di/providers.dart';
|
||||||
import '../../../../core/services/print_service.dart';
|
import '../../../../core/services/sunmi_service.dart';
|
||||||
import '../../../../core/storage/secure_storage.dart';
|
import '../../../../core/storage/secure_storage.dart';
|
||||||
import '../../../../core/utils/text_utils.dart';
|
import '../../../../core/utils/text_utils.dart';
|
||||||
import '../../../users/domain/entities/user_entity.dart';
|
import '../../../users/domain/entities/user_entity.dart';
|
||||||
@@ -86,27 +87,25 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-select warehouse user based on stored email from login
|
/// Auto-select warehouse user based on stored user ID from login
|
||||||
Future<void> _autoSelectWarehouseUser() async {
|
Future<void> _autoSelectWarehouseUser() async {
|
||||||
try {
|
try {
|
||||||
// Get stored email from secure storage
|
// Get stored current user ID from secure storage
|
||||||
final secureStorage = SecureStorage();
|
final secureStorage = SecureStorage();
|
||||||
final storedEmail = await secureStorage.getEmail();
|
final currentUserId = await secureStorage.getCurrentUserId();
|
||||||
|
|
||||||
if (storedEmail == null || storedEmail.isEmpty) {
|
if (currentUserId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print(storedEmail);
|
|
||||||
|
|
||||||
// Get all warehouse users
|
// Get all warehouse users
|
||||||
final warehouseUsers = ref.read(usersListProvider)
|
final warehouseUsers = ref.read(usersListProvider)
|
||||||
.where((user) => user.isWareHouseUser)
|
.where((user) => user.isWareHouseUser)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Find user with matching email
|
// Find user with matching ID
|
||||||
final matchingUsers = warehouseUsers
|
final matchingUsers = warehouseUsers
|
||||||
.where((user) => user.email.toLowerCase() == storedEmail.toLowerCase())
|
.where((user) => user.id == currentUserId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final matchingUser = matchingUsers.isNotEmpty ? matchingUsers.first : null;
|
final matchingUser = matchingUsers.isNotEmpty ? matchingUsers.first : null;
|
||||||
@@ -141,6 +140,205 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearUserSelections() {
|
||||||
|
setState(() {
|
||||||
|
_selectedWarehouseUser = null;
|
||||||
|
_selectedEmployee = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showBarcodeScanner() {
|
||||||
|
final controller = MobileScannerController(
|
||||||
|
formats: const [BarcodeFormat.code128],
|
||||||
|
facing: CameraFacing.back,
|
||||||
|
);
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) => Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.7,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade900,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.qr_code_scanner,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Quét mã vạch',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
controller.dispose();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Scanner
|
||||||
|
Expanded(
|
||||||
|
child: MobileScanner(
|
||||||
|
controller: controller,
|
||||||
|
onDetect: (capture) {
|
||||||
|
final List<Barcode> barcodes = capture.barcodes;
|
||||||
|
if (barcodes.isNotEmpty) {
|
||||||
|
final barcode = barcodes.first.rawValue;
|
||||||
|
if (barcode != null) {
|
||||||
|
controller.dispose();
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleScannedBarcode(barcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Instructions
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: Colors.grey.shade900,
|
||||||
|
child: const Text(
|
||||||
|
'Đặt mã vạch Code 128 vào khung để quét',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).whenComplete(() => controller.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleScannedBarcode(String barcode) async {
|
||||||
|
// Parse barcode to extract productId and optional stageId
|
||||||
|
// Format 1: "123" (only productId)
|
||||||
|
// Format 2: "123-456" (productId-stageId)
|
||||||
|
|
||||||
|
int? productId;
|
||||||
|
int? stageId;
|
||||||
|
|
||||||
|
if (barcode.contains('-')) {
|
||||||
|
// Format: productId-stageId
|
||||||
|
final parts = barcode.split('-');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
productId = int.tryParse(parts[0]);
|
||||||
|
stageId = int.tryParse(parts[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Format: productId only
|
||||||
|
productId = int.tryParse(barcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productId == null) {
|
||||||
|
// Invalid barcode format
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Định dạng mã vạch không hợp lệ: "$barcode"'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
if (!mounted) return;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the provider key and load product detail
|
||||||
|
setState(() {
|
||||||
|
_providerKey = '${widget.warehouseId}_$productId';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear current selections
|
||||||
|
_clearControllers();
|
||||||
|
|
||||||
|
// Load product detail data from API
|
||||||
|
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||||
|
widget.warehouseId,
|
||||||
|
productId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dismiss loading dialog
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// If stageId is provided, auto-select that stage
|
||||||
|
if (stageId != null && mounted) {
|
||||||
|
final stages = ref.read(productDetailProvider(_providerKey)).stages;
|
||||||
|
final stageIndex = stages.indexWhere(
|
||||||
|
(stage) => stage.productStageId == stageId,
|
||||||
|
);
|
||||||
|
if (stageIndex != -1) {
|
||||||
|
ref.read(productDetailProvider(_providerKey).notifier).selectStage(stageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Đã tải sản phẩm ID: $productId'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Dismiss loading dialog
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Lỗi khi tải sản phẩm: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -162,8 +360,13 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('${operationTitle} ${productName}'),
|
title: Text('$operationTitle: $productName'),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: _showBarcodeScanner,
|
||||||
|
tooltip: 'Quét mã vạch',
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
@@ -215,7 +418,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Error',
|
'Lỗi',
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
@@ -452,7 +655,9 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
_buildUserDropdown(
|
_buildUserDropdown(
|
||||||
label: 'Nhân viên *',
|
label: 'Nhân viên *',
|
||||||
value: _selectedEmployee,
|
value: _selectedEmployee,
|
||||||
users: ref.watch(usersListProvider),
|
users: ref.watch(usersListProvider)
|
||||||
|
.where((user) => user.roleId == 2)
|
||||||
|
.toList(),
|
||||||
onChanged: (user) {
|
onChanged: (user) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedEmployee = user;
|
_selectedEmployee = user;
|
||||||
@@ -524,7 +729,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
: 'P${stage.productId}';
|
: 'P${stage.productId}';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await PrintService.printWarehouseExport(
|
await SunmiService.printWarehouseExport(
|
||||||
context: context,
|
context: context,
|
||||||
warehouseName: widget.warehouseName,
|
warehouseName: widget.warehouseName,
|
||||||
productId: stage.productId,
|
productId: stage.productId,
|
||||||
@@ -543,7 +748,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Error printing: ${e.toString()}'),
|
content: Text('Lỗi khi in: ${e.toString()}'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
@@ -583,6 +788,8 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Show loading dialog
|
// Show loading dialog
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -644,7 +851,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(_) {
|
(_) async {
|
||||||
// Success - show success message
|
// Success - show success message
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -655,14 +862,17 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear the text fields after successful add
|
// Print before saving to API
|
||||||
_clearControllers();
|
await _printQuantities(stage);
|
||||||
|
|
||||||
// Refresh the product detail to show updated quantities
|
// Refresh the product detail to show updated quantities
|
||||||
ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||||
widget.warehouseId,
|
widget.warehouseId,
|
||||||
widget.productId,
|
widget.productId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Do NOT clear quantity/weight fields - keep them for reference
|
||||||
|
// User can manually clear them if needed using the 'C' button
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import '../models/user_model.dart';
|
|||||||
abstract class UsersRemoteDataSource {
|
abstract class UsersRemoteDataSource {
|
||||||
/// Fetch all users from the API
|
/// Fetch all users from the API
|
||||||
Future<List<UserModel>> getUsers();
|
Future<List<UserModel>> getUsers();
|
||||||
|
|
||||||
|
/// Get current logged-in user
|
||||||
|
Future<UserModel> getCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of UsersRemoteDataSource using ApiClient
|
/// Implementation of UsersRemoteDataSource using ApiClient
|
||||||
@@ -54,4 +57,41 @@ class UsersRemoteDataSourceImpl implements UsersRemoteDataSource {
|
|||||||
throw ServerException('Failed to get users: ${e.toString()}');
|
throw ServerException('Failed to get users: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel> getCurrentUser() async {
|
||||||
|
try {
|
||||||
|
// Make API call to get current user
|
||||||
|
final response = await apiClient.get(ApiEndpoints.getCurrentUser);
|
||||||
|
|
||||||
|
// Parse the API response using ApiResponse wrapper
|
||||||
|
final apiResponse = ApiResponse.fromJson(
|
||||||
|
response.data as Map<String, dynamic>,
|
||||||
|
(json) => UserModel.fromJson(json as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the API call was successful
|
||||||
|
if (apiResponse.isSuccess && apiResponse.value != null) {
|
||||||
|
return apiResponse.value!;
|
||||||
|
} else {
|
||||||
|
// Throw exception with error message from API
|
||||||
|
throw ServerException(
|
||||||
|
apiResponse.errors.isNotEmpty
|
||||||
|
? apiResponse.errors.first
|
||||||
|
: 'Failed to get current user',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Re-throw ServerException as-is
|
||||||
|
if (e is ServerException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// Re-throw NetworkException as-is
|
||||||
|
if (e is NetworkException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// Wrap other exceptions in ServerException
|
||||||
|
throw ServerException('Failed to get current user: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../../../../core/di/providers.dart';
|
import '../../../../core/di/providers.dart';
|
||||||
import '../../../../core/router/app_router.dart';
|
import '../../../../core/router/app_router.dart';
|
||||||
|
import '../../../../core/storage/secure_storage.dart';
|
||||||
import '../widgets/warehouse_card.dart';
|
import '../widgets/warehouse_card.dart';
|
||||||
import '../widgets/warehouse_drawer.dart';
|
import '../widgets/warehouse_drawer.dart';
|
||||||
|
|
||||||
@@ -28,12 +29,34 @@ class _WarehouseSelectionPageState
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Load warehouses when page is first created
|
// Load warehouses when page is first created
|
||||||
Future.microtask(() {
|
Future.microtask(() async {
|
||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
// Users are automatically loaded from local storage by UsersNotifier
|
// Users are automatically loaded from local storage by UsersNotifier
|
||||||
|
|
||||||
|
// Get current user and store user ID
|
||||||
|
await _getCurrentUserAndStoreId();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get current user from API and store user ID in secure storage
|
||||||
|
Future<void> _getCurrentUserAndStoreId() async {
|
||||||
|
try {
|
||||||
|
final secureStorage = SecureStorage();
|
||||||
|
final usersDataSource = ref.read(usersRemoteDataSourceProvider);
|
||||||
|
|
||||||
|
// Call API to get current user
|
||||||
|
final currentUser = await usersDataSource.getCurrentUser();
|
||||||
|
|
||||||
|
// Store the current user ID
|
||||||
|
await secureStorage.saveCurrentUserId(currentUser.id);
|
||||||
|
|
||||||
|
debugPrint('Current user ID stored: ${currentUser.id}');
|
||||||
|
} catch (e) {
|
||||||
|
// Silently fail - this is not critical
|
||||||
|
debugPrint('Error getting current user: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Watch warehouse state
|
// Watch warehouse state
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MyApp extends ConsumerWidget {
|
|||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'Warehouse Manager',
|
title: 'MinhThu',
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: AppTheme.darkTheme,
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
|
|||||||
@@ -1029,6 +1029,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
sunmi_printer_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sunmi_printer_plus
|
||||||
|
sha256: "77293b7da16bdf3805c5a24ea41731978e8a31da99c3fca38a658a0778450b78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies:
|
|||||||
pdf: ^3.11.3
|
pdf: ^3.11.3
|
||||||
barcode_widget: ^2.0.4
|
barcode_widget: ^2.0.4
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
|
sunmi_printer_plus: ^4.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user