From e14ae56c3cddebb49d71088f0ca71c915ed474e1 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Tue, 28 Oct 2025 14:24:58 +0700 Subject: [PATCH] update --- ...otlin-compiler-12659883282835606996.salive | 0 android/app/build.gradle | 6 +- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 4 +- android/settings.gradle.kts | 2 +- ios/Podfile.lock | 8 +- lib/core/router/app_router.dart | 9 +- .../pages/product_detail_page.dart | 231 +++++++++++++----- .../presentation/pages/products_page.dart | 143 +++++++++++ 10 files changed, 329 insertions(+), 78 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-12659883282835606996.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-12659883282835606996.salive b/android/.kotlin/sessions/kotlin-compiler-12659883282835606996.salive deleted file mode 100644 index e69de29..0000000 diff --git a/android/app/build.gradle b/android/app/build.gradle index 7a6b004..2c23cf1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,12 +28,12 @@ android { ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_20 - targetCompatibility JavaVersion.VERSION_20 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '20' + jvmTarget = '17' } sourceSets { diff --git a/android/build.gradle b/android/build.gradle index 1bdb07d..0487c26 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.6.0' } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 5d6560a..e6045a9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip diff --git a/android/settings.gradle b/android/settings.gradle index 2b25ebe..dab2b0b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.21" apply false } include ":app" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index fb605bc..e986a21 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -20,7 +20,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.9.1" apply false - id("org.jetbrains.kotlin.android") version "2.1.0" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false } include(":app") diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f8dee5d..2500113 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -33,10 +33,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 61bbabb..f55f5e9 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -125,8 +125,9 @@ class AppRouter { /// Product Detail Route /// Path: /product-detail - /// Takes warehouseId, productId, and warehouseName as extra parameter + /// Takes warehouseId, productId, warehouseName, and optional stageId as extra parameter /// Shows detailed information for a specific product + /// If stageId is provided, only that stage is shown, otherwise all stages are shown GoRoute( path: '/product-detail', name: 'product-detail', @@ -147,6 +148,8 @@ class AppRouter { final warehouseId = params['warehouseId'] as int?; final productId = params['productId'] as int?; final warehouseName = params['warehouseName'] as String?; + // Extract optional stageId + final stageId = params['stageId'] as int?; // Validate parameters if (warehouseId == null || productId == null || warehouseName == null) { @@ -162,6 +165,7 @@ class AppRouter { warehouseId: warehouseId, productId: productId, warehouseName: warehouseName, + stageId: stageId, ); }, ), @@ -375,10 +379,12 @@ extension AppRouterExtension on BuildContext { /// [warehouseId] - ID of the warehouse /// [productId] - ID of the product to view /// [warehouseName] - Name of the warehouse (for display) + /// [stageId] - Optional ID of specific stage to show (if null, show all stages) void goToProductDetail({ required int warehouseId, required int productId, required String warehouseName, + int? stageId, }) { push( '/product-detail', @@ -386,6 +392,7 @@ extension AppRouterExtension on BuildContext { 'warehouseId': warehouseId, 'productId': productId, 'warehouseName': warehouseName, + if (stageId != null) 'stageId': stageId, }, ); } diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart index d380bb4..5ef8751 100644 --- a/lib/features/products/presentation/pages/product_detail_page.dart +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -6,16 +6,19 @@ import '../../domain/entities/product_stage_entity.dart'; /// Product detail page /// Displays product stages as chips and shows selected stage information +/// If [stageId] is provided, only that stage is shown, otherwise all stages are shown class ProductDetailPage extends ConsumerStatefulWidget { final int warehouseId; final int productId; final String warehouseName; + final int? stageId; const ProductDetailPage({ super.key, required this.warehouseId, required this.productId, required this.warehouseName, + this.stageId, }); @override @@ -31,11 +34,22 @@ class _ProductDetailPageState extends ConsumerState { _providerKey = '${widget.warehouseId}_${widget.productId}'; // Load product stages when page is initialized - Future.microtask(() { - ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail( + Future.microtask(() async { + await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail( widget.warehouseId, widget.productId, ); + + // If stageId is provided, auto-select that stage + if (widget.stageId != null) { + final stages = ref.read(productDetailProvider(_providerKey)).stages; + final stageIndex = stages.indexWhere( + (stage) => stage.productStageId == widget.stageId, + ); + if (stageIndex != -1) { + ref.read(productDetailProvider(_providerKey).notifier).selectStage(stageIndex); + } + } }); } @@ -166,76 +180,162 @@ class _ProductDetailPageState extends ConsumerState { ); } + // Filter stages if stageId is provided + final displayStages = widget.stageId != null + ? stages.where((stage) => stage.productStageId == widget.stageId).toList() + : stages; + + // When stageId is provided but no matching stage found + if (widget.stageId != null && displayStages.isEmpty && stages.isNotEmpty) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: theme.colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Stage Not Found', + 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.', + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back), + label: const Text('Go Back'), + ), + ], + ), + ), + ), + ); + } + return RefreshIndicator( onRefresh: _onRefresh, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stage chips section - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - border: Border( - bottom: BorderSide( - color: theme.colorScheme.outline.withValues(alpha: 0.2), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Production Stages (${stages.length})', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + if (displayStages.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.2), ), ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate(stages.length, (index) { - final stage = stages[index]; - final isSelected = index == selectedIndex; - - return FilterChip( - selected: isSelected, - label: Text(stage.displayName), - onSelected: (_) { - ref - .read(productDetailProvider(_providerKey).notifier) - .selectStage(index); - }, - backgroundColor: theme.colorScheme.surface, - selectedColor: theme.colorScheme.primaryContainer, - checkmarkColor: theme.colorScheme.primary, - labelStyle: TextStyle( - color: isSelected - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurface, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.stageId != null + ? 'Selected Stage' + : 'Production Stages (${displayStages.length})', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), ), - ); - }), - ), - ], + if (widget.stageId != null) ...[ + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'ID: ${widget.stageId}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(displayStages.length, (index) { + final stage = displayStages[index]; + // When stageId is provided, check if this stage matches the stageId + // Otherwise, check if the index matches the selected index + final isSelected = widget.stageId != null + ? stage.productStageId == widget.stageId + : stages.indexOf(stage) == selectedIndex; + + return FilterChip( + selected: isSelected, + label: Text(stage.displayName), + onSelected: widget.stageId == null + ? (_) { + ref + .read(productDetailProvider(_providerKey).notifier) + .selectStage(stages.indexOf(stage)); + } + : null, // Disable selection when specific stage is shown + backgroundColor: theme.colorScheme.surface, + selectedColor: theme.colorScheme.primaryContainer, + checkmarkColor: theme.colorScheme.primary, + labelStyle: TextStyle( + color: isSelected + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ); + }), + ), + ], + ), ), - ), // Stage details section Expanded( - child: selectedStage == null - ? const Center(child: Text('No stage selected')) - : SingleChildScrollView( + child: () { + // When stageId is provided, use the filtered stage + final stageToShow = widget.stageId != null && displayStages.isNotEmpty + ? displayStages.first + : selectedStage; + + if (stageToShow == null) { + return const Center(child: Text('No stage selected')); + } + + return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stage header - _buildStageHeader(selectedStage, theme), + _buildStageHeader(stageToShow, theme), const SizedBox(height: 16), // Quantity information @@ -244,16 +344,16 @@ class _ProductDetailPageState extends ConsumerState { title: 'Quantities', icon: Icons.inventory_outlined, children: [ - _buildInfoRow('Passed Quantity', '${selectedStage.passedQuantity}'), + _buildInfoRow('Passed Quantity', '${stageToShow.passedQuantity}'), _buildInfoRow( 'Passed Weight', - '${selectedStage.passedQuantityWeight.toStringAsFixed(2)} kg', + '${stageToShow.passedQuantityWeight.toStringAsFixed(2)} kg', ), const Divider(height: 24), - _buildInfoRow('Issued Quantity', '${selectedStage.issuedQuantity}'), + _buildInfoRow('Issued Quantity', '${stageToShow.issuedQuantity}'), _buildInfoRow( 'Issued Weight', - '${selectedStage.issuedQuantityWeight.toStringAsFixed(2)} kg', + '${stageToShow.issuedQuantityWeight.toStringAsFixed(2)} kg', ), ], ), @@ -265,21 +365,22 @@ class _ProductDetailPageState extends ConsumerState { title: 'Stage Information', icon: Icons.info_outlined, children: [ - _buildInfoRow('Product ID', '${selectedStage.productId}'), - if (selectedStage.productStageId != null) - _buildInfoRow('Stage ID', '${selectedStage.productStageId}'), - if (selectedStage.actionTypeId != null) - _buildInfoRow('Action Type ID', '${selectedStage.actionTypeId}'), - _buildInfoRow('Stage Name', selectedStage.displayName), + _buildInfoRow('Product ID', '${stageToShow.productId}'), + if (stageToShow.productStageId != null) + _buildInfoRow('Stage ID', '${stageToShow.productStageId}'), + if (stageToShow.actionTypeId != null) + _buildInfoRow('Action Type ID', '${stageToShow.actionTypeId}'), + _buildInfoRow('Stage Name', stageToShow.displayName), ], ), const SizedBox(height: 16), // Status indicators - _buildStatusCards(selectedStage, theme), + _buildStatusCards(stageToShow, theme), ], ), - ), + ); + }(), ), ], ), diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index a3d4c15..28d2ccc 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; import '../../../../core/di/providers.dart'; import '../../../../core/router/app_router.dart'; @@ -41,6 +42,141 @@ class _ProductsPageState extends ConsumerState { await ref.read(productsProvider.notifier).refreshProducts(); } + void _showBarcodeScanner() { + final controller = MobileScannerController( + formats: const [BarcodeFormat.code128], + facing: CameraFacing.back, + ); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: Row( + children: [ + const Icon( + Icons.qr_code_scanner, + color: Colors.white, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Scan Barcode', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () { + controller.dispose(); + Navigator.pop(context); + }, + ), + ], + ), + ), + // Scanner + Expanded( + child: MobileScanner( + controller: controller, + onDetect: (capture) { + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + final barcode = barcodes.first.rawValue; + if (barcode != null) { + controller.dispose(); + Navigator.pop(context); + _handleScannedBarcode(barcode); + } + } + }, + ), + ), + // Instructions + Container( + padding: const EdgeInsets.all(16), + color: Colors.grey.shade900, + child: const Text( + 'Position the Code 128 barcode within the frame to scan', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ).whenComplete(() => controller.dispose()); + } + + void _handleScannedBarcode(String barcode) { + // Parse barcode to extract productId and optional stageId + // Format 1: "123" (only productId) + // Format 2: "123-456" (productId-stageId) + + int? productId; + int? stageId; + + if (barcode.contains('-')) { + // Format: productId-stageId + final parts = barcode.split('-'); + if (parts.length == 2) { + productId = int.tryParse(parts[0]); + stageId = int.tryParse(parts[1]); + } + } else { + // Format: productId only + productId = int.tryParse(barcode); + } + + if (productId == null) { + // Invalid barcode format + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invalid barcode format: "$barcode"'), + backgroundColor: Colors.red, + action: SnackBarAction( + label: 'OK', + textColor: Colors.white, + onPressed: () {}, + ), + ), + ); + return; + } + + // Navigate to product detail with productId and optional stageId + context.goToProductDetail( + warehouseId: widget.warehouseId, + productId: productId, + warehouseName: widget.warehouseName, + stageId: stageId, + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -83,6 +219,13 @@ class _ProductsPageState extends ConsumerState { products: products, theme: theme, ), + floatingActionButton: products.isNotEmpty + ? FloatingActionButton( + onPressed: _showBarcodeScanner, + tooltip: 'Scan Barcode', + child: const Icon(Icons.qr_code_scanner), + ) + : null, ); }