Compare commits
2 Commits
73b77c27de
...
2905668358
| Author | SHA1 | Date | |
|---|---|---|---|
| 2905668358 | |||
| f32e1c16fb |
@@ -33,10 +33,10 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
|
||||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class AppRouter {
|
|||||||
context.go('/warehouses');
|
context.go('/warehouses');
|
||||||
});
|
});
|
||||||
return const _ErrorScreen(
|
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');
|
context.go('/warehouses');
|
||||||
});
|
});
|
||||||
return const _ErrorScreen(
|
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');
|
context.go('/warehouses');
|
||||||
});
|
});
|
||||||
return const _ErrorScreen(
|
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) {
|
errorBuilder: (context, state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Page Not Found'),
|
title: const Text('Không tìm thấy trang'),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -176,12 +176,12 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Page Not Found',
|
'Không tìm thấy trang',
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
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(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -190,7 +190,7 @@ class AppRouter {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => context.go('/login'),
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Error'),
|
title: const Text('Lỗi'),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -296,7 +296,7 @@ class _ErrorScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Navigation Error',
|
'Lỗi điều hướng',
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -313,7 +313,7 @@ class _ErrorScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => context.go('/warehouses'),
|
onPressed: () => context.go('/warehouses'),
|
||||||
child: const Text('Go to Warehouses'),
|
child: const Text('Về trang kho'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -50,15 +50,9 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, void>> logout() async {
|
Future<Either<Failure, void>> logout() async {
|
||||||
try {
|
try {
|
||||||
// Call remote data source to logout (optional - can fail silently)
|
// Just clear access token from secure storage
|
||||||
try {
|
// No API call needed
|
||||||
await remoteDataSource.logout();
|
await secureStorage.clearTokens();
|
||||||
} catch (e) {
|
|
||||||
// Ignore remote logout errors, still clear local data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all local authentication data
|
|
||||||
await secureStorage.clearAll();
|
|
||||||
|
|
||||||
return const Right(null);
|
return const Right(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
|
|
||||||
// App title
|
// App title
|
||||||
Text(
|
Text(
|
||||||
'Warehouse Manager',
|
'Quản lý kho',
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
@@ -90,7 +90,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text(
|
Text(
|
||||||
'Login to continue',
|
'Đăng nhập để tiếp tục',
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -129,24 +129,11 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
///
|
///
|
||||||
/// Clears authentication data and returns to initial state
|
/// Clears authentication data and returns to initial state
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
// Set loading state
|
// Clear tokens from secure storage
|
||||||
state = state.copyWith(isLoading: true, error: null);
|
await logoutUseCase();
|
||||||
|
|
||||||
// Call logout use case
|
// Always reset to initial state (clear local data even if API call fails)
|
||||||
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)
|
|
||||||
state = const AuthState.initial();
|
state = const AuthState.initial();
|
||||||
},
|
|
||||||
(_) {
|
|
||||||
// Logout successful - reset to initial state
|
|
||||||
state = const AuthState.initial();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check authentication status on app start
|
/// Check authentication status on app start
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
enabled: !widget.isLoading,
|
enabled: !widget.isLoading,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Username',
|
labelText: 'Tên đăng nhập',
|
||||||
hintText: 'Enter your username',
|
hintText: 'Nhập tên đăng nhập',
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -67,10 +67,10 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
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) {
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -84,8 +84,8 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
enabled: !widget.isLoading,
|
enabled: !widget.isLoading,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Mật khẩu',
|
||||||
hintText: 'Enter your password',
|
hintText: 'Nhập mật khẩu',
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -107,10 +107,10 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
onFieldSubmitted: (_) => _handleSubmit(),
|
onFieldSubmitted: (_) => _handleSubmit(),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Password is required';
|
return 'Vui lòng nhập mật khẩu';
|
||||||
}
|
}
|
||||||
if (value.length < 6) {
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -131,7 +131,7 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.login),
|
: 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(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ProductStageEntity extends Equatable {
|
|||||||
|
|
||||||
/// Get display name for the stage
|
/// Get display name for the stage
|
||||||
/// Returns "No Stage" if stageName is null
|
/// 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)
|
/// Check if this is a valid stage (has a stage name)
|
||||||
bool get hasStage => stageName != null && stageName!.isNotEmpty;
|
bool get hasStage => stageName != null && stageName!.isNotEmpty;
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
// await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||||
widget.warehouseId,
|
// widget.warehouseId,
|
||||||
widget.productId,
|
// widget.productId,
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearControllers() {
|
void _clearControllers() {
|
||||||
@@ -114,7 +114,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
final productName = stages.isNotEmpty ? stages.first.productName : 'Product';
|
final productName = stages.isNotEmpty ? stages.first.productName : 'Product';
|
||||||
|
|
||||||
// Capitalize first letter of operation type
|
// Capitalize first letter of operation type
|
||||||
final operationTitle = widget.operationType == 'import' ? 'Import' : 'Export';
|
final operationTitle = widget.operationType == 'import' ? 'Nhập' : 'Xuất';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -137,7 +137,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Làm mới',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -195,7 +195,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
icon: const Icon(Icons.refresh),
|
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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No stages found',
|
'Không tìm thấy công đoạn',
|
||||||
style: theme.textTheme.titleLarge,
|
style: theme.textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -244,14 +244,14 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Stage Not Found',
|
'Không tìm thấy công đoạn',
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
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,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
@@ -259,7 +259,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
icon: const Icon(Icons.arrow_back),
|
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: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.stageId != null
|
widget.stageId != null
|
||||||
? 'Selected Stage'
|
? 'Công đoạn'
|
||||||
: 'Production Stages (${displayStages.length})',
|
: 'Công đoạn (${displayStages.length})',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -368,7 +368,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
: selectedStage;
|
: selectedStage;
|
||||||
|
|
||||||
if (stageToShow == null) {
|
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(
|
return SingleChildScrollView(
|
||||||
@@ -383,32 +383,32 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
title: 'Stage Information',
|
title: 'Thông tin công đoạn',
|
||||||
icon: Icons.info_outlined,
|
icon: Icons.info_outlined,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow('Product ID', '${stageToShow.productId}'),
|
_buildInfoRow('Mã sản phẩm', '${stageToShow.productId}'),
|
||||||
if (stageToShow.productStageId != null)
|
if (stageToShow.productStageId != null)
|
||||||
_buildInfoRow('Stage ID', '${stageToShow.productStageId}'),
|
_buildInfoRow('Mã công đoạn', '${stageToShow.productStageId}'),
|
||||||
if (stageToShow.actionTypeId != null)
|
if (stageToShow.actionTypeId != null)
|
||||||
_buildInfoRow('Action Type ID', '${stageToShow.actionTypeId}'),
|
_buildInfoRow('Mã loại thao tác', '${stageToShow.actionTypeId}'),
|
||||||
_buildInfoRow('Stage Name', stageToShow.displayName),
|
_buildInfoRow('Tên công đoạn', stageToShow.displayName),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Current Quantity information
|
// Current Quantity information
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
title: 'Current Quantities',
|
title: 'Số lượng hiện tại',
|
||||||
icon: Icons.info_outlined,
|
icon: Icons.info_outlined,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'),
|
_buildInfoRow('Số lượng đạt', '${stageToShow.passedQuantity}'),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Passed Weight',
|
'Khối lượng đạt',
|
||||||
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
'${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg',
|
||||||
),
|
),
|
||||||
_buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'),
|
_buildInfoRow('Số lượng lỗi', '${stageToShow.issuedQuantity}'),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Issued Weight',
|
'Khối lượng lỗi',
|
||||||
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
'${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -417,29 +417,29 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
// Add New Quantities section
|
// Add New Quantities section
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
title: 'Add New Quantities',
|
title: 'Thêm số lượng mới',
|
||||||
icon: Icons.add_circle_outline,
|
icon: Icons.add_circle_outline,
|
||||||
children: [
|
children: [
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Passed Quantity',
|
label: 'Số lượng đạt',
|
||||||
controller: _passedQuantityController,
|
controller: _passedQuantityController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Passed Weight (kg)',
|
label: 'Khối lượng đạt (kg)',
|
||||||
controller: _passedWeightController,
|
controller: _passedWeightController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
theme: theme,
|
theme: theme,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Issued Quantity',
|
label: 'Số lượng lỗi',
|
||||||
controller: _issuedQuantityController,
|
controller: _issuedQuantityController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
label: 'Issued Weight (kg)',
|
label: 'Khối lượng lỗi (kg)',
|
||||||
controller: _issuedWeightController,
|
controller: _issuedWeightController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
theme: theme,
|
theme: theme,
|
||||||
@@ -450,7 +450,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
_buildSectionCard(theme: theme, title: "Nhân viên", icon: Icons.people, children: [
|
_buildSectionCard(theme: theme, title: "Nhân viên", icon: Icons.people, children: [
|
||||||
// Warehouse User Dropdown
|
// Warehouse User Dropdown
|
||||||
_buildUserDropdown(
|
_buildUserDropdown(
|
||||||
label: 'Warehouse User',
|
label: 'Người dùng kho',
|
||||||
value: _selectedWarehouseUser,
|
value: _selectedWarehouseUser,
|
||||||
users: ref.watch(usersListProvider)
|
users: ref.watch(usersListProvider)
|
||||||
.where((user) => user.isWareHouseUser)
|
.where((user) => user.isWareHouseUser)
|
||||||
@@ -464,7 +464,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
),
|
),
|
||||||
// All Employees Dropdown
|
// All Employees Dropdown
|
||||||
_buildUserDropdown(
|
_buildUserDropdown(
|
||||||
label: 'Employee',
|
label: 'Nhân viên',
|
||||||
value: _selectedEmployee,
|
value: _selectedEmployee,
|
||||||
users: ref.watch(usersListProvider),
|
users: ref.watch(usersListProvider),
|
||||||
onChanged: (user) {
|
onChanged: (user) {
|
||||||
@@ -485,7 +485,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => _printQuantities(stageToShow),
|
onPressed: () => _printQuantities(stageToShow),
|
||||||
icon: const Icon(Icons.print),
|
icon: const Icon(Icons.print),
|
||||||
label: const Text('Print'),
|
label: const Text('In'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
),
|
),
|
||||||
@@ -495,7 +495,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () => _addNewQuantities(stageToShow),
|
onPressed: () => _addNewQuantities(stageToShow),
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
label: const Text('Save'),
|
label: const Text('Lưu'),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
),
|
),
|
||||||
@@ -517,7 +517,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
// TODO: Implement print functionality
|
// TODO: Implement print functionality
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Print functionality coming soon'),
|
content: Text('Tính năng in đang phát triển'),
|
||||||
duration: Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -535,7 +535,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
issuedQuantity == 0 && issuedWeight == 0.0) {
|
issuedQuantity == 0 && issuedWeight == 0.0) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
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,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -546,7 +546,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
if (_selectedEmployee == null || _selectedWarehouseUser == null) {
|
if (_selectedEmployee == null || _selectedWarehouseUser == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
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,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -607,7 +607,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Failed to add quantities: ${failure.message}'),
|
content: Text('Lỗi khi thêm số lượng: ${failure.message}'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
@@ -619,7 +619,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Quantities added successfully!'),
|
content: Text('Đã thêm số lượng thành công!'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
@@ -644,7 +644,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Error: ${e.toString()}'),
|
content: Text('Lỗi: ${e.toString()}'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
@@ -686,7 +686,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Product ID: ${stage.productId}',
|
'Sản phẩm ID: ${stage.productId}',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -892,7 +892,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
hint: Text('Select $label'),
|
hint: Text('Chọn $label'),
|
||||||
items: users.map((user) {
|
items: users.map((user) {
|
||||||
return DropdownMenuItem<UserEntity>(
|
return DropdownMenuItem<UserEntity>(
|
||||||
value: user,
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Scan Barcode',
|
'Quét mã vạch',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -161,7 +161,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
color: Colors.grey.shade900,
|
color: Colors.grey.shade900,
|
||||||
child: const Text(
|
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(
|
style: TextStyle(
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -199,7 +199,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
// Invalid barcode format
|
// Invalid barcode format
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Invalid barcode format: "$barcode"'),
|
content: Text('Định dạng mã vạch không hợp lệ: "$barcode"'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: 'OK',
|
label: 'OK',
|
||||||
@@ -212,11 +212,12 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to product detail with productId and optional stageId
|
// Navigate to product detail with productId and optional stageId
|
||||||
|
// Use the currently selected tab's operation type
|
||||||
context.goToProductDetail(
|
context.goToProductDetail(
|
||||||
warehouseId: widget.warehouseId,
|
warehouseId: widget.warehouseId,
|
||||||
productId: productId,
|
productId: productId,
|
||||||
warehouseName: widget.warehouseName,
|
warehouseName: widget.warehouseName,
|
||||||
operationType: widget.operationType,
|
operationType: _currentOperationType,
|
||||||
stageId: stageId,
|
stageId: stageId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,7 +251,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Products',
|
'Sản phẩm',
|
||||||
style: textTheme.titleMedium,
|
style: textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -265,7 +266,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Làm mới',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
@@ -273,11 +274,11 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(Icons.arrow_downward),
|
icon: Icon(Icons.arrow_downward),
|
||||||
text: 'Import',
|
text: 'Nhập kho',
|
||||||
),
|
),
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(Icons.arrow_upward),
|
icon: Icon(Icons.arrow_upward),
|
||||||
text: 'Export',
|
text: 'Xuất kho',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -291,7 +292,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
floatingActionButton: products.isNotEmpty
|
floatingActionButton: products.isNotEmpty
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
onPressed: _showBarcodeScanner,
|
onPressed: _showBarcodeScanner,
|
||||||
tooltip: 'Scan Barcode',
|
tooltip: 'Quét mã vạch',
|
||||||
child: const Icon(Icons.qr_code_scanner),
|
child: const Icon(Icons.qr_code_scanner),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -336,14 +337,14 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_currentOperationType == 'import'
|
_currentOperationType == 'import'
|
||||||
? 'Import Products'
|
? 'Nhập kho'
|
||||||
: 'Export Products',
|
: 'Xuất kho',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Warehouse: ${widget.warehouseName}',
|
'Kho: ${widget.warehouseName}',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -385,7 +386,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
CircularProgressIndicator(),
|
||||||
SizedBox(height: 16),
|
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),
|
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,
|
||||||
),
|
),
|
||||||
@@ -421,7 +422,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
icon: const Icon(Icons.refresh),
|
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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No Products',
|
'Không có sản phẩm',
|
||||||
style: theme.textTheme.titleLarge,
|
style: theme.textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
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,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
@@ -459,7 +460,7 @@ class _ProductsPageState extends ConsumerState<ProductsPage>
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _onRefresh,
|
onPressed: _onRefresh,
|
||||||
icon: const Icon(Icons.refresh),
|
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(
|
return ProductListItem(
|
||||||
product: product,
|
product: product,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to product detail page
|
// Navigate to product detail page with current tab's operation type
|
||||||
context.goToProductDetail(
|
context.goToProductDetail(
|
||||||
warehouseId: widget.warehouseId,
|
warehouseId: widget.warehouseId,
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
warehouseName: widget.warehouseName,
|
warehouseName: widget.warehouseName,
|
||||||
operationType: widget.operationType,
|
operationType: _currentOperationType,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Code: ${product.code}',
|
'Mã: ${product.code}',
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
@@ -65,7 +65,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Active',
|
'Hoạt động',
|
||||||
style: textTheme.labelSmall?.copyWith(
|
style: textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -84,7 +84,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _InfoItem(
|
child: _InfoItem(
|
||||||
label: 'Weight',
|
label: 'Khối lượng',
|
||||||
value: '${product.weight.toStringAsFixed(2)} kg',
|
value: '${product.weight.toStringAsFixed(2)} kg',
|
||||||
icon: Icons.fitness_center,
|
icon: Icons.fitness_center,
|
||||||
),
|
),
|
||||||
@@ -92,7 +92,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _InfoItem(
|
child: _InfoItem(
|
||||||
label: 'Pieces',
|
label: 'Số lượng',
|
||||||
value: product.pieces.toString(),
|
value: product.pieces.toString(),
|
||||||
icon: Icons.inventory_2,
|
icon: Icons.inventory_2,
|
||||||
),
|
),
|
||||||
@@ -107,7 +107,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _InfoItem(
|
child: _InfoItem(
|
||||||
label: 'In Stock (Pieces)',
|
label: 'Tồn kho (SL)',
|
||||||
value: product.piecesInStock.toString(),
|
value: product.piecesInStock.toString(),
|
||||||
icon: Icons.warehouse,
|
icon: Icons.warehouse,
|
||||||
color: product.piecesInStock > 0
|
color: product.piecesInStock > 0
|
||||||
@@ -118,7 +118,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _InfoItem(
|
child: _InfoItem(
|
||||||
label: 'In Stock (Weight)',
|
label: 'Tồn kho (KL)',
|
||||||
value: '${product.weightInStock.toStringAsFixed(2)} kg',
|
value: '${product.weightInStock.toStringAsFixed(2)} kg',
|
||||||
icon: Icons.scale,
|
icon: Icons.scale,
|
||||||
color: product.weightInStock > 0
|
color: product.weightInStock > 0
|
||||||
@@ -142,7 +142,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Conversion Rate',
|
'Tỷ lệ chuyển đổi',
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -170,7 +170,7 @@ class ProductListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Barcode: ${product.barcode}',
|
'Mã vạch: ${product.barcode}',
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ class UsersNotifier extends StateNotifier<UsersState> {
|
|||||||
UsersNotifier({
|
UsersNotifier({
|
||||||
required this.getUsersUseCase,
|
required this.getUsersUseCase,
|
||||||
required this.syncUsersUseCase,
|
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)
|
/// Get users from local storage (or API if not cached)
|
||||||
Future<void> getUsers() async {
|
Future<void> getUsers() async {
|
||||||
|
|||||||
@@ -4,6 +4,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 '../widgets/warehouse_card.dart';
|
import '../widgets/warehouse_card.dart';
|
||||||
|
import '../widgets/warehouse_drawer.dart';
|
||||||
|
|
||||||
/// Warehouse selection page
|
/// Warehouse selection page
|
||||||
/// Displays a list of warehouses and allows user to select one
|
/// Displays a list of warehouses and allows user to select one
|
||||||
@@ -26,11 +27,10 @@ class _WarehouseSelectionPageState
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Load warehouses and sync users when page is first created
|
// Load warehouses when page is first created
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
// Sync users from API and save to local storage
|
// Users are automatically loaded from local storage by UsersNotifier
|
||||||
ref.read(usersProvider.notifier).syncUsers();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +44,18 @@ class _WarehouseSelectionPageState
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Select Warehouse'),
|
title: const Text('Chọn kho'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
},
|
},
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Làm mới',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
drawer: const WarehouseDrawer(),
|
||||||
body: _buildBody(
|
body: _buildBody(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
error: error,
|
error: error,
|
||||||
@@ -77,7 +78,7 @@ class _WarehouseSelectionPageState
|
|||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
CircularProgressIndicator(),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text('Loading warehouses...'),
|
Text('Đang tải danh sách kho...'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -98,7 +99,7 @@ class _WarehouseSelectionPageState
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Error Loading Warehouses',
|
'Lỗi tải danh sách kho',
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -113,7 +114,7 @@ class _WarehouseSelectionPageState
|
|||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh),
|
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),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No Warehouses Available',
|
'Không có kho',
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'There are no warehouses to display.',
|
'Không có kho nào để hiển thị.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
@@ -151,7 +152,7 @@ class _WarehouseSelectionPageState
|
|||||||
ref.read(warehouseProvider.notifier).loadWarehouses();
|
ref.read(warehouseProvider.notifier).loadWarehouses();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: const Text('Refresh'),
|
label: const Text('Làm mới'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class WarehouseCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Code: ${warehouse.code}',
|
'Mã: ${warehouse.code}',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -68,7 +68,7 @@ class WarehouseCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Items: ${warehouse.totalCount}',
|
'Sản phẩm: ${warehouse.totalCount}',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -104,7 +104,7 @@ class WarehouseCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'NG Warehouse',
|
'Kho NG',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: colorScheme.onErrorContainer,
|
color: colorScheme.onErrorContainer,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user