Compare commits

...

2 Commits

Author SHA1 Message Date
2905668358 fix 2025-10-28 23:56:47 +07:00
f32e1c16fb fix 2025-10-28 23:46:07 +07:00
14 changed files with 384 additions and 193 deletions

View File

@@ -33,10 +33,10 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -72,7 +72,7 @@ class AppRouter {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Warehouse data is required',
message: 'Yêu cầu dữ liệu kho',
);
}
@@ -103,7 +103,7 @@ class AppRouter {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Invalid product parameters',
message: 'Tham số sản phẩm không hợp lệ',
);
}
@@ -143,7 +143,7 @@ class AppRouter {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Invalid product detail parameters',
message: 'Tham số chi tiết sản phẩm không hợp lệ',
);
}
@@ -163,7 +163,7 @@ class AppRouter {
errorBuilder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Page Not Found'),
title: const Text('Không tìm thấy trang'),
),
body: Center(
child: Column(
@@ -176,12 +176,12 @@ class AppRouter {
),
const SizedBox(height: 16),
Text(
'Page Not Found',
'Không tìm thấy trang',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'The page "${state.uri.path}" does not exist.',
'Trang "${state.uri.path}" không tồn tại.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@@ -190,7 +190,7 @@ class AppRouter {
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/login'),
child: const Text('Go to Login'),
child: const Text('Về trang đăng nhập'),
),
],
),
@@ -283,7 +283,7 @@ class _ErrorScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Error'),
title: const Text('Lỗi'),
),
body: Center(
child: Column(
@@ -296,7 +296,7 @@ class _ErrorScreen extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
'Navigation Error',
'Lỗi điều hướng',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
@@ -313,7 +313,7 @@ class _ErrorScreen extends StatelessWidget {
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/warehouses'),
child: const Text('Go to Warehouses'),
child: const Text('Về trang kho'),
),
],
),

View File

@@ -50,15 +50,9 @@ class AuthRepositoryImpl implements AuthRepository {
@override
Future<Either<Failure, void>> logout() async {
try {
// Call remote data source to logout (optional - can fail silently)
try {
await remoteDataSource.logout();
} catch (e) {
// Ignore remote logout errors, still clear local data
}
// Clear all local authentication data
await secureStorage.clearAll();
// Just clear access token from secure storage
// No API call needed
await secureStorage.clearTokens();
return const Right(null);
} catch (e) {

View File

@@ -78,7 +78,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
// App title
Text(
'Warehouse Manager',
'Quản lý kho',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
@@ -90,7 +90,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
// Subtitle
Text(
'Login to continue',
'Đăng nhập để tiếp tục',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),

View File

@@ -129,24 +129,11 @@ class AuthNotifier extends StateNotifier<AuthState> {
///
/// Clears authentication data and returns to initial state
Future<void> logout() async {
// Set loading state
state = state.copyWith(isLoading: true, error: null);
// Clear tokens from secure storage
await logoutUseCase();
// Call logout use case
final result = await logoutUseCase();
// Handle result
result.fold(
(failure) {
// Logout failed - but still reset to initial state
// (local data should be cleared even if API call fails)
// Always reset to initial state (clear local data even if API call fails)
state = const AuthState.initial();
},
(_) {
// Logout successful - reset to initial state
state = const AuthState.initial();
},
);
}
/// Check authentication status on app start

View File

@@ -56,8 +56,8 @@ class _LoginFormState extends State<LoginForm> {
controller: _usernameController,
enabled: !widget.isLoading,
decoration: InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
labelText: 'Tên đăng nhập',
hintText: 'Nhập tên đăng nhập',
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
@@ -67,10 +67,10 @@ class _LoginFormState extends State<LoginForm> {
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Username is required';
return 'Vui lòng nhập tên đăng nhập';
}
if (value.trim().length < 3) {
return 'Username must be at least 3 characters';
return 'Tên đăng nhập phải có ít nhất 3 ký tự';
}
return null;
},
@@ -84,8 +84,8 @@ class _LoginFormState extends State<LoginForm> {
enabled: !widget.isLoading,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
labelText: 'Mật khẩu',
hintText: 'Nhập mật khẩu',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
@@ -107,10 +107,10 @@ class _LoginFormState extends State<LoginForm> {
onFieldSubmitted: (_) => _handleSubmit(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
return 'Vui lòng nhập mật khẩu';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
return 'Mật khẩu phải có ít nhất 6 ký tự';
}
return null;
},
@@ -131,7 +131,7 @@ class _LoginFormState extends State<LoginForm> {
),
)
: const Icon(Icons.login),
label: Text(widget.isLoading ? 'Logging in...' : 'Login'),
label: Text(widget.isLoading ? 'Đang đăng nhập...' : 'Đăng nhập'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(

View File

@@ -33,7 +33,7 @@ class ProductStageEntity extends Equatable {
/// Get display name for the stage
/// Returns "No Stage" if stageName is null
String get displayName => stageName ?? 'No Stage';
String get displayName => stageName ?? 'Không tên';
/// Check if this is a valid stage (has a stage name)
bool get hasStage => stageName != null && stageName!.isNotEmpty;

View File

@@ -80,10 +80,10 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
}
Future<void> _onRefresh() async {
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
widget.warehouseId,
widget.productId,
);
// await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
// widget.warehouseId,
// widget.productId,
// );
}
void _clearControllers() {
@@ -114,7 +114,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
final productName = stages.isNotEmpty ? stages.first.productName : 'Product';
// Capitalize first letter of operation type
final operationTitle = widget.operationType == 'import' ? 'Import' : 'Export';
final operationTitle = widget.operationType == 'import' ? 'Nhập' : 'Xuất';
return Scaffold(
appBar: AppBar(
@@ -137,7 +137,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _onRefresh,
tooltip: 'Refresh',
tooltip: 'Làm mới',
),
],
),
@@ -195,7 +195,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
FilledButton.icon(
onPressed: _onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
label: const Text('Thử lại'),
),
],
),
@@ -215,7 +215,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
),
const SizedBox(height: 16),
Text(
'No stages found',
'Không tìm thấy công đoạn',
style: theme.textTheme.titleLarge,
),
],
@@ -244,14 +244,14 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
),
const SizedBox(height: 16),
Text(
'Stage Not Found',
'Không tìm thấy công đoạn',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.error,
),
),
const SizedBox(height: 8),
Text(
'Stage with ID ${widget.stageId} was not found in this product.',
'Công đoạn với ID ${widget.stageId} không được tìm thấy trong sản phẩm này.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
@@ -259,7 +259,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
FilledButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back),
label: const Text('Go Back'),
label: const Text('Quay lại'),
),
],
),
@@ -293,8 +293,8 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
children: [
Text(
widget.stageId != null
? 'Selected Stage'
: 'Production Stages (${displayStages.length})',
? 'Công đoạn'
: 'Công đoạn (${displayStages.length})',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -368,7 +368,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
: selectedStage;
if (stageToShow == null) {
return const Center(child: Text('No stage selected'));
return const Center(child: Text('Chưa chọn công đoạn'));
}
return SingleChildScrollView(
@@ -383,32 +383,32 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
_buildSectionCard(
theme: theme,
title: 'Stage Information',
title: 'Thông tin công đoạn',
icon: Icons.info_outlined,
children: [
_buildInfoRow('Product ID', '${stageToShow.productId}'),
_buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'),
if (stageToShow.productStageId != null)
_buildInfoRow('Stage ID', '${stageToShow.productStageId}'),
_buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'),
if (stageToShow.actionTypeId != null)
_buildInfoRow('Action Type ID', '${stageToShow.actionTypeId}'),
_buildInfoRow('Stage Name', stageToShow.displayName),
_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: 'Current Quantities',
title: 'Số lượng hiện tại',
icon: Icons.info_outlined,
children: [
_buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'),
_buildInfoRow('Số lượng đạt', '${stageToShow.passedQuantity}'),
_buildInfoRow(
'Passed Weight',
'Khối lượng đạt',
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
),
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
_buildInfoRow('Số lượng lỗi', '${stageToShow.issuedQuantity}'),
_buildInfoRow(
'Issued Weight',
'Khối lượng lỗi',
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
),
],
@@ -417,29 +417,29 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
// Add New Quantities section
_buildSectionCard(
theme: theme,
title: 'Add New Quantities',
title: 'Thêm số lượng mới',
icon: Icons.add_circle_outline,
children: [
_buildTextField(
label: 'Passed Quantity',
label: 'Số lượng đạt',
controller: _passedQuantityController,
keyboardType: TextInputType.number,
theme: theme,
),
_buildTextField(
label: 'Passed Weight (kg)',
label: 'Khối lượng đạt (kg)',
controller: _passedWeightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
theme: theme,
),
_buildTextField(
label: 'Issued Quantity',
label: 'Số lượng lỗi',
controller: _issuedQuantityController,
keyboardType: TextInputType.number,
theme: theme,
),
_buildTextField(
label: 'Issued Weight (kg)',
label: 'Khối lượng lỗi (kg)',
controller: _issuedWeightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
theme: theme,
@@ -450,7 +450,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
_buildSectionCard(theme: theme, title: "Nhân viên", icon: Icons.people, children: [
// Warehouse User Dropdown
_buildUserDropdown(
label: 'Warehouse User',
label: 'Người dùng kho',
value: _selectedWarehouseUser,
users: ref.watch(usersListProvider)
.where((user) => user.isWareHouseUser)
@@ -464,7 +464,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
),
// All Employees Dropdown
_buildUserDropdown(
label: 'Employee',
label: 'Nhân viên',
value: _selectedEmployee,
users: ref.watch(usersListProvider),
onChanged: (user) {
@@ -485,7 +485,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
child: OutlinedButton.icon(
onPressed: () => _printQuantities(stageToShow),
icon: const Icon(Icons.print),
label: const Text('Print'),
label: const Text('In'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
@@ -495,7 +495,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
child: FilledButton.icon(
onPressed: () => _addNewQuantities(stageToShow),
icon: const Icon(Icons.save),
label: const Text('Save'),
label: const Text('Lưu'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
@@ -517,7 +517,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
// TODO: Implement print functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Print functionality coming soon'),
content: Text('Tính năng in đang phát triển'),
duration: Duration(seconds: 2),
),
);
@@ -535,7 +535,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
issuedQuantity == 0 && issuedWeight == 0.0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter at least one quantity or weight value'),
content: Text('Vui lòng nhập ít nhất một giá trị số lượng hoặc khối lượng'),
backgroundColor: Colors.orange,
),
);
@@ -546,7 +546,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (_selectedEmployee == null || _selectedWarehouseUser == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select both Employee and Warehouse User'),
content: Text('Vui lòng chọn cả Nhân viên và Người dùng kho'),
backgroundColor: Colors.orange,
),
);
@@ -607,7 +607,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to add quantities: ${failure.message}'),
content: Text('Lỗi khi thêm số lượng: ${failure.message}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
@@ -619,7 +619,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Quantities added successfully!'),
content: Text('Đã thêm số lượng thành công!'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
@@ -644,7 +644,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}'),
content: Text('Lỗi: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
@@ -686,7 +686,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
),
const SizedBox(height: 4),
Text(
'Product ID: ${stage.productId}',
'Sản phẩm ID: ${stage.productId}',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@@ -892,7 +892,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
vertical: 12,
),
),
hint: Text('Select $label'),
hint: Text('Chọn $label'),
items: users.map((user) {
return DropdownMenuItem<UserEntity>(
value: user,
@@ -921,61 +921,4 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
}
}
Widget _buildStatusCards(ProductStageEntity stage, ThemeData theme) {
return Row(
children: [
Expanded(
child: Card(
color: stage.hasPassedQuantity
? Colors.green.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(
stage.hasPassedQuantity ? Icons.check_circle : Icons.cancel,
color: stage.hasPassedQuantity ? Colors.green : Colors.grey,
size: 32,
),
const SizedBox(height: 8),
Text(
'Has Passed',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Card(
color: stage.hasIssuedQuantity
? Colors.blue.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(
stage.hasIssuedQuantity ? Icons.check_circle : Icons.cancel,
color: stage.hasIssuedQuantity ? Colors.blue : Colors.grey,
size: 32,
),
const SizedBox(height: 8),
Text(
'Has Issued',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
),
),
),
),
],
);
}
}

View File

@@ -121,7 +121,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
const SizedBox(width: 12),
const Expanded(
child: Text(
'Scan Barcode',
'Quét mã vạch',
style: TextStyle(
color: Colors.white,
fontSize: 18,
@@ -161,7 +161,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
padding: const EdgeInsets.all(16),
color: Colors.grey.shade900,
child: const Text(
'Position the Code 128 barcode within the frame to scan',
'Đặt mã vạch Code 128 vào khung để quét',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
@@ -199,7 +199,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
// Invalid barcode format
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Invalid barcode format: "$barcode"'),
content: Text('Định dạng mã vạch không hợp lệ: "$barcode"'),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'OK',
@@ -212,11 +212,12 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
}
// Navigate to product detail with productId and optional stageId
// Use the currently selected tab's operation type
context.goToProductDetail(
warehouseId: widget.warehouseId,
productId: productId,
warehouseName: widget.warehouseName,
operationType: widget.operationType,
operationType: _currentOperationType,
stageId: stageId,
);
}
@@ -250,7 +251,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Products',
'Sản phẩm',
style: textTheme.titleMedium,
),
Text(
@@ -265,7 +266,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _onRefresh,
tooltip: 'Refresh',
tooltip: 'Làm mới',
),
],
bottom: TabBar(
@@ -273,11 +274,11 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
tabs: const [
Tab(
icon: Icon(Icons.arrow_downward),
text: 'Import',
text: 'Nhập kho',
),
Tab(
icon: Icon(Icons.arrow_upward),
text: 'Export',
text: 'Xuất kho',
),
],
),
@@ -291,7 +292,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
floatingActionButton: products.isNotEmpty
? FloatingActionButton(
onPressed: _showBarcodeScanner,
tooltip: 'Scan Barcode',
tooltip: 'Quét mã vạch',
child: const Icon(Icons.qr_code_scanner),
)
: null,
@@ -336,14 +337,14 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
children: [
Text(
_currentOperationType == 'import'
? 'Import Products'
: 'Export Products',
? 'Nhập kho'
: 'Xuất kho',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Warehouse: ${widget.warehouseName}',
'Kho: ${widget.warehouseName}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@@ -385,7 +386,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading products...'),
Text('Đang tải sản phẩm...'),
],
),
);
@@ -406,7 +407,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
),
const SizedBox(height: 16),
Text(
'Error',
'Lỗi',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.error,
),
@@ -421,7 +422,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
FilledButton.icon(
onPressed: _onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
label: const Text('Thử lại'),
),
],
),
@@ -444,12 +445,12 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
),
const SizedBox(height: 16),
Text(
'No Products',
'Không có sản phẩm',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'No products found for this warehouse and operation type.',
'Không tìm thấy sản phẩm nào cho kho và loại thao tác này.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
@@ -459,7 +460,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
FilledButton.icon(
onPressed: _onRefresh,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
label: const Text('Làm mới'),
),
],
),
@@ -478,12 +479,12 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
return ProductListItem(
product: product,
onTap: () {
// Navigate to product detail page
// Navigate to product detail page with current tab's operation type
context.goToProductDetail(
warehouseId: widget.warehouseId,
productId: product.id,
warehouseName: widget.warehouseName,
operationType: widget.operationType,
operationType: _currentOperationType,
);
},
);

View File

@@ -45,7 +45,7 @@ class ProductListItem extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'Code: ${product.code}',
': ${product.code}',
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary,
),
@@ -65,7 +65,7 @@ class ProductListItem extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Active',
'Hoạt động',
style: textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
@@ -84,7 +84,7 @@ class ProductListItem extends StatelessWidget {
children: [
Expanded(
child: _InfoItem(
label: 'Weight',
label: 'Khối lượng',
value: '${product.weight.toStringAsFixed(2)} kg',
icon: Icons.fitness_center,
),
@@ -92,7 +92,7 @@ class ProductListItem extends StatelessWidget {
const SizedBox(width: 16),
Expanded(
child: _InfoItem(
label: 'Pieces',
label: 'Số lượng',
value: product.pieces.toString(),
icon: Icons.inventory_2,
),
@@ -107,7 +107,7 @@ class ProductListItem extends StatelessWidget {
children: [
Expanded(
child: _InfoItem(
label: 'In Stock (Pieces)',
label: 'Tồn kho (SL)',
value: product.piecesInStock.toString(),
icon: Icons.warehouse,
color: product.piecesInStock > 0
@@ -118,7 +118,7 @@ class ProductListItem extends StatelessWidget {
const SizedBox(width: 16),
Expanded(
child: _InfoItem(
label: 'In Stock (Weight)',
label: 'Tồn kho (KL)',
value: '${product.weightInStock.toStringAsFixed(2)} kg',
icon: Icons.scale,
color: product.weightInStock > 0
@@ -142,7 +142,7 @@ class ProductListItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Conversion Rate',
'Tỷ lệ chuyển đổi',
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
@@ -170,7 +170,7 @@ class ProductListItem extends StatelessWidget {
),
const SizedBox(width: 4),
Text(
'Barcode: ${product.barcode}',
'Mã vạch: ${product.barcode}',
style: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),

View File

@@ -12,7 +12,10 @@ class UsersNotifier extends StateNotifier<UsersState> {
UsersNotifier({
required this.getUsersUseCase,
required this.syncUsersUseCase,
}) : super(const UsersState());
}) : super(const UsersState()) {
// Load local users on initialization
getUsers();
}
/// Get users from local storage (or API if not cached)
Future<void> getUsers() async {

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart';
import '../../../../core/router/app_router.dart';
import '../widgets/warehouse_card.dart';
import '../widgets/warehouse_drawer.dart';
/// Warehouse selection page
/// Displays a list of warehouses and allows user to select one
@@ -26,11 +27,10 @@ class _WarehouseSelectionPageState
@override
void initState() {
super.initState();
// Load warehouses and sync users when page is first created
// Load warehouses when page is first created
Future.microtask(() {
ref.read(warehouseProvider.notifier).loadWarehouses();
// Sync users from API and save to local storage
ref.read(usersProvider.notifier).syncUsers();
// Users are automatically loaded from local storage by UsersNotifier
});
}
@@ -44,17 +44,18 @@ class _WarehouseSelectionPageState
return Scaffold(
appBar: AppBar(
title: const Text('Select Warehouse'),
title: const Text('Chọn kho'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
tooltip: 'Refresh',
tooltip: 'Làm mới',
),
],
),
drawer: const WarehouseDrawer(),
body: _buildBody(
isLoading: isLoading,
error: error,
@@ -77,7 +78,7 @@ class _WarehouseSelectionPageState
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading warehouses...'),
Text('Đang tải danh sách kho...'),
],
),
);
@@ -98,7 +99,7 @@ class _WarehouseSelectionPageState
),
const SizedBox(height: 16),
Text(
'Error Loading Warehouses',
'Lỗi tải danh sách kho',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
@@ -113,7 +114,7 @@ class _WarehouseSelectionPageState
ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
label: const Text('Thử lại'),
),
],
),
@@ -136,12 +137,12 @@ class _WarehouseSelectionPageState
),
const SizedBox(height: 16),
Text(
'No Warehouses Available',
'Không có kho',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'There are no warehouses to display.',
'Không có kho nào để hiển thị.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
@@ -151,7 +152,7 @@ class _WarehouseSelectionPageState
ref.read(warehouseProvider.notifier).loadWarehouses();
},
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
label: const Text('Làm mới'),
),
],
),

View File

@@ -49,7 +49,7 @@ class WarehouseCard extends StatelessWidget {
),
const SizedBox(width: 4),
Text(
'Code: ${warehouse.code}',
': ${warehouse.code}',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -68,7 +68,7 @@ class WarehouseCard extends StatelessWidget {
),
const SizedBox(width: 4),
Text(
'Items: ${warehouse.totalCount}',
'Sản phẩm: ${warehouse.totalCount}',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -104,7 +104,7 @@ class WarehouseCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
'NG Warehouse',
'Kho NG',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onErrorContainer,
fontWeight: FontWeight.bold,

View File

@@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/providers.dart';
/// Drawer for warehouse selection page
/// Contains app settings and sync options
class WarehouseDrawer extends ConsumerWidget {
const WarehouseDrawer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final authState = ref.watch(authProvider);
final usersState = ref.watch(usersProvider);
final user = authState.user;
return Drawer(
child: SafeArea(
child: Column(
children: [
// Header with user info
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 32,
backgroundColor: theme.colorScheme.primary,
child: Icon(
Icons.person,
size: 32,
color: theme.colorScheme.onPrimary,
),
),
const SizedBox(height: 16),
Text(
user?.username ?? 'User',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 4),
Text(
'Quản lý kho',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onPrimaryContainer.withValues(alpha: 0.8),
),
),
],
),
),
// Menu items
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
// Sync Users button
ListTile(
leading: Icon(
Icons.sync,
color: theme.colorScheme.primary,
),
title: const Text('Đồng bộ người dùng'),
subtitle: Text(
usersState.users.isEmpty
? 'Chưa có dữ liệu'
: '${usersState.users.length} người dùng',
style: theme.textTheme.bodySmall,
),
trailing: usersState.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
Icons.cloud_download,
color: theme.colorScheme.secondary,
),
onTap: usersState.isLoading
? null
: () async {
// Close drawer first
Navigator.of(context).pop();
// Show loading indicator
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 16),
Text('Đang đồng bộ...'),
],
),
duration: Duration(seconds: 2),
),
);
// Sync users from API
await ref.read(usersProvider.notifier).syncUsers();
// Show success or error message
if (context.mounted) {
final error = ref.read(usersProvider).error;
if (error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 16),
Expanded(child: Text('Lỗi: $error')),
],
),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
),
);
} else {
final count = ref.read(usersProvider).users.length;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 16),
Text('Đã đồng bộ $count người dùng'),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
}
},
),
const Divider(),
// Settings (placeholder)
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Cài đặt'),
subtitle: const Text('Tùy chỉnh ứng dụng'),
onTap: () {
Navigator.of(context).pop();
// TODO: Navigate to settings page
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng đang phát triển'),
),
);
},
),
// About (placeholder)
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Thông tin'),
subtitle: const Text('Về ứng dụng'),
onTap: () {
Navigator.of(context).pop();
showAboutDialog(
context: context,
applicationName: 'Quản lý kho',
applicationVersion: '1.0.0',
applicationIcon: const Icon(Icons.warehouse, size: 48),
children: [
const Text('Hệ thống quản lý kho và theo dõi sản phẩm.'),
],
);
},
),
],
),
),
// Logout button at bottom
const Divider(height: 1),
ListTile(
leading: Icon(
Icons.logout,
color: theme.colorScheme.error,
),
title: Text(
'Đăng xuất',
style: TextStyle(
color: theme.colorScheme.error,
fontWeight: FontWeight.bold,
),
),
onTap: () async {
// Capture references BEFORE closing drawer (drawer will be disposed)
final authNotifier = ref.read(authProvider.notifier);
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
navigator.pop(); // Close drawer
// Show logout confirmation dialog and get result
final shouldLogout = await _showLogoutDialog(context);
// If user confirmed, logout and navigate to login
if (shouldLogout == true) {
await authNotifier.logout();
// Navigate to login screen using captured router
router.go('/login');
}
},
),
],
),
),
);
}
Future<bool?> _showLogoutDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Đăng xuất'),
content: const Text('Bạn có chắc chắn muốn đăng xuất?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Hủy'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('Đăng xuất'),
),
],
),
);
}
}