Compare commits

...

5 Commits

Author SHA1 Message Date
Phuoc Nguyen
2ff639fc42 sunmi 2025-11-04 18:10:54 +07:00
Phuoc Nguyen
1cfdd2c0c6 update. save => print 2025-11-04 09:29:35 +07:00
ff25363a19 sunmi 2025-11-04 08:12:13 +07:00
9df4b79a66 store current user id 2025-11-03 20:59:51 +07:00
2a6ec8f6b8 print => save 2025-11-02 23:24:50 +07:00
12 changed files with 357 additions and 31 deletions

View File

@@ -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

View File

@@ -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

View 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;
}
}
}

View File

@@ -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();

View File

@@ -127,4 +127,25 @@ curl --request GET \
--header 'Sec-Fetch-Dest: empty' \ --header 'Sec-Fetch-Dest: empty' \
--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 ''

View File

@@ -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

View File

@@ -4,8 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import '../../../../core/di/providers.dart'; import '../../../../core/di/providers.dart';
import '../../../../core/router/app_router.dart'; import '../../../../core/services/sunmi_service.dart';
import '../../../../core/services/print_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';
@@ -88,14 +87,14 @@ 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;
} }
@@ -104,9 +103,9 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
.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;
@@ -419,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,
), ),
@@ -656,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;
@@ -728,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,
@@ -747,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),
), ),
@@ -787,6 +788,8 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
return; return;
} }
// Show loading dialog // Show loading dialog
showDialog( showDialog(
context: context, context: context,
@@ -859,22 +862,15 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
), ),
); );
// Print before saving to API
await _printQuantities(stage);
// Refresh the product detail to show updated quantities // Refresh the product detail to show updated quantities
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail( await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
widget.warehouseId, widget.warehouseId,
widget.productId, widget.productId,
); );
// Get updated stage data
final updatedStages = ref.read(productDetailProvider(_providerKey)).stages;
final updatedStage = updatedStages.firstWhere(
(s) => s.productStageId == stage.productStageId,
orElse: () => stage,
);
// Automatically print after successful save
await _printQuantities(updatedStage);
// Do NOT clear quantity/weight fields - keep them for reference // Do NOT clear quantity/weight fields - keep them for reference
// User can manually clear them if needed using the 'C' button // User can manually clear them if needed using the 'C' button
} }

View File

@@ -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()}');
}
}
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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: