diff --git a/Manager/lib/db/database_helper.dart b/Manager/lib/db/database_helper.dart index 6b5499f..81621e5 100644 --- a/Manager/lib/db/database_helper.dart +++ b/Manager/lib/db/database_helper.dart @@ -31,7 +31,7 @@ class DatabaseHelper { return await openDatabase( path, - version: 4, // Incremented version + version: 5, // Incremented version for search fields onCreate: _createDB, onUpgrade: _upgradeDB, ); @@ -46,7 +46,9 @@ class DatabaseHelper { last_updated INTEGER NOT NULL, brand TEXT, size TEXT, - type TEXT + type TEXT, + pinyin TEXT, + initials TEXT ) '''); @@ -79,6 +81,10 @@ class DatabaseHelper { if (oldVersion < 4) { await db.execute('ALTER TABLE kiosks ADD COLUMN device_id TEXT'); } + if (oldVersion < 5) { + await db.execute('ALTER TABLE products ADD COLUMN pinyin TEXT'); + await db.execute('ALTER TABLE products ADD COLUMN initials TEXT'); + } } // Product Methods diff --git a/Manager/lib/l10n/app_en.arb b/Manager/lib/l10n/app_en.arb index fcc5409..bc757d3 100644 --- a/Manager/lib/l10n/app_en.arb +++ b/Manager/lib/l10n/app_en.arb @@ -56,6 +56,7 @@ "settingsSaved": "Settings saved", "productApiUrlLabel": "Product API URL", "productApiUrlHint": "https://barcode100.market.alicloudapi.com/getBarcode?Code=", + "searchProducts": "Search Products", "cancel": "Cancel", "remove": "Remove", "connectKioskToEdit": "Connect to a kiosk to add or edit products.", diff --git a/Manager/lib/l10n/app_localizations.dart b/Manager/lib/l10n/app_localizations.dart index a52e022..c09cb19 100644 --- a/Manager/lib/l10n/app_localizations.dart +++ b/Manager/lib/l10n/app_localizations.dart @@ -440,6 +440,12 @@ abstract class AppLocalizations { /// **'https://barcode100.market.alicloudapi.com/getBarcode?Code='** String get productApiUrlHint; + /// No description provided for @searchProducts. + /// + /// In en, this message translates to: + /// **'Search Products'** + String get searchProducts; + /// No description provided for @cancel. /// /// In en, this message translates to: diff --git a/Manager/lib/l10n/app_localizations_en.dart b/Manager/lib/l10n/app_localizations_en.dart index 8034c35..37cdf1b 100644 --- a/Manager/lib/l10n/app_localizations_en.dart +++ b/Manager/lib/l10n/app_localizations_en.dart @@ -197,6 +197,9 @@ class AppLocalizationsEn extends AppLocalizations { String get productApiUrlHint => 'https://barcode100.market.alicloudapi.com/getBarcode?Code='; + @override + String get searchProducts => 'Search Products'; + @override String get cancel => 'Cancel'; diff --git a/Manager/lib/l10n/app_localizations_zh.dart b/Manager/lib/l10n/app_localizations_zh.dart index 3b265be..4a0b7b9 100644 --- a/Manager/lib/l10n/app_localizations_zh.dart +++ b/Manager/lib/l10n/app_localizations_zh.dart @@ -196,6 +196,9 @@ class AppLocalizationsZh extends AppLocalizations { String get productApiUrlHint => 'https://barcode100.market.alicloudapi.com/getBarcode?Code='; + @override + String get searchProducts => '搜索商品'; + @override String get cancel => '取消'; diff --git a/Manager/lib/l10n/app_zh.arb b/Manager/lib/l10n/app_zh.arb index 08507f0..ae59ae3 100644 --- a/Manager/lib/l10n/app_zh.arb +++ b/Manager/lib/l10n/app_zh.arb @@ -56,6 +56,7 @@ "settingsSaved": "设置已保存", "productApiUrlLabel": "商品API地址", "productApiUrlHint": "https://barcode100.market.alicloudapi.com/getBarcode?Code=", + "searchProducts": "搜索商品", "cancel": "取消", "remove": "移除", "connectKioskToEdit": "请先连接自助终端后再添加或编辑商品。", diff --git a/Manager/lib/models/product.dart b/Manager/lib/models/product.dart index d64cb50..59e967b 100644 --- a/Manager/lib/models/product.dart +++ b/Manager/lib/models/product.dart @@ -18,6 +18,10 @@ class Product { final String? size; @HiveField(6) final String? type; + @HiveField(7) + final String? pinyin; + @HiveField(8) + final String? initials; Product({ required this.barcode, @@ -27,6 +31,8 @@ class Product { this.brand, this.size, this.type, + this.pinyin, + this.initials, }); factory Product.fromJson(Map json) { @@ -38,6 +44,8 @@ class Product { brand: json['brand'], size: json['size'], type: json['type'], + pinyin: json['pinyin'], + initials: json['initials'], ); } @@ -50,6 +58,8 @@ class Product { 'brand': brand, 'size': size, 'type': type, + 'pinyin': pinyin, + 'initials': initials, }; } } diff --git a/Manager/lib/screens/product_form_screen.dart b/Manager/lib/screens/product_form_screen.dart index f026d47..b4a1ce7 100644 --- a/Manager/lib/screens/product_form_screen.dart +++ b/Manager/lib/screens/product_form_screen.dart @@ -6,7 +6,8 @@ import 'package:manager/services/api_service.dart'; import 'package:manager/db/database_helper.dart'; import 'package:manager/services/kiosk_connection_service.dart'; import 'package:manager/services/kiosk_client/kiosk_client.dart'; -import 'package:manager/l10n/app_localizations.dart'; // Re-added +import 'package:manager/l10n/app_localizations.dart'; +import 'package:lpinyin/lpinyin.dart'; // Proper import placement class ProductFormScreen extends StatefulWidget { final String? initialBarcode; @@ -178,11 +179,26 @@ class _ProductFormScreenState extends State { ? _barcodeController.text : await DatabaseHelper.instance.getNextNoBarcodeId()) : _barcodeController.text.trim(); + + final name = _nameController.text; + // Generate Pinyin and Initials + String? pinyin; + String? initials; + if (name.isNotEmpty) { + // PinyinHelper.getPinyin returns full pinyin with separator + // e.g. "ke kou ke le" + pinyin = PinyinHelper.getPinyin(name, separator: ' ', format: PinyinFormat.WITHOUT_TONE).toLowerCase(); + // Initials: "kkkl" + initials = PinyinHelper.getShortPinyin(name).toLowerCase(); + } + final product = Product( barcode: barcode, - name: _nameController.text, + name: name, price: double.parse(_priceController.text.trim()), lastUpdated: DateTime.now().millisecondsSinceEpoch, + pinyin: pinyin, + initials: initials, ); try { diff --git a/Manager/lib/screens/product_list_screen.dart b/Manager/lib/screens/product_list_screen.dart index 61451b1..62246de 100644 --- a/Manager/lib/screens/product_list_screen.dart +++ b/Manager/lib/screens/product_list_screen.dart @@ -16,23 +16,48 @@ class ProductListScreen extends StatefulWidget { class _ProductListScreenState extends State { List _products = []; - bool _isLoading = true; + bool _isLoading = false; final KioskConnectionService _connectionService = KioskConnectionService(); final KioskClientService _kioskService = KioskClientService(); + final TextEditingController _searchController = TextEditingController(); + List _filteredProducts = []; @override void initState() { super.initState(); _connectionService.addListener(_onConnectionChange); + _searchController.addListener(_onSearchChanged); _loadProducts(); } @override void dispose() { _connectionService.removeListener(_onConnectionChange); + _searchController.dispose(); super.dispose(); } + void _onSearchChanged() { + final query = _searchController.text.trim().toLowerCase(); + if (query.isEmpty) { + setState(() => _filteredProducts = _products); + return; + } + setState(() { + _filteredProducts = _products.where((p) { + final name = p.name.toLowerCase(); + final barcode = p.barcode.toLowerCase(); + final pinyin = p.pinyin?.toLowerCase() ?? ''; + final initials = p.initials?.toLowerCase() ?? ''; + + return name.contains(query) || + barcode.contains(query) || + pinyin.contains(query) || + initials.contains(query); + }).toList(); + }); + } + void _onConnectionChange() { if (mounted) setState(() {}); } @@ -45,8 +70,13 @@ class _ProductListScreenState extends State { if (!mounted) return; setState(() { _products = products; + _filteredProducts = products; _isLoading = false; }); + // Re-apply search if exists + if (_searchController.text.isNotEmpty) { + _onSearchChanged(); + } } catch (_) { if (!mounted) return; setState(() => _isLoading = false); @@ -147,36 +177,51 @@ class _ProductListScreenState extends State { appBar: AppBar(title: Text(l10n.addProduct)), // Reuse "Add Product" label or "Products" body: _isLoading ? const Center(child: CircularProgressIndicator()) - : _products.isEmpty - ? Center(child: Text(l10n.noProductsFound)) - : ListView.builder( - itemCount: _products.length, - itemBuilder: (context, index) { - final product = _products[index]; - final tile = ListTile( - key: ValueKey(product.barcode), - title: Text(product.name), - subtitle: Text(product.barcode), - trailing: Text(currencyFormat.format(product.price)), - onTap: isConnected ? () => _navigateToAddEdit(barcode: product.barcode) : null, - enabled: isConnected, - ); - - if (!isConnected) { - return tile; - } - - return Dismissible( - key: ValueKey('dismiss_${product.barcode}'), - direction: DismissDirection.endToStart, - confirmDismiss: (_) => _confirmDelete(product), - onDismissed: (_) { - setState(() { - _products.removeAt(index); - }); - _deleteProduct(product); - }, - background: const SizedBox.shrink(), + : Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: l10n.searchProducts, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + ), + ), + ), + Expanded( + child: _filteredProducts.isEmpty + ? Center(child: Text(l10n.noProductsFound)) + : ListView.builder( + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + final tile = ListTile( + key: ValueKey(product.barcode), + title: Text(product.name), + subtitle: Text(product.barcode), + trailing: Text(currencyFormat.format(product.price)), + onTap: isConnected ? () => _navigateToAddEdit(barcode: product.barcode) : null, + enabled: isConnected, + ); + + if (!isConnected) { + return tile; + } + + return Dismissible( + key: ValueKey('dismiss_${product.barcode}'), + direction: DismissDirection.endToStart, + confirmDismiss: (_) => _confirmDelete(product), + onDismissed: (_) { + setState(() { + _products.remove(product); // Remove from main list + _filteredProducts.removeAt(index); + }); + _deleteProduct(product); + }, + background: const SizedBox.shrink(), secondaryBackground: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric(horizontal: 20), @@ -190,6 +235,9 @@ class _ProductListScreenState extends State { ); }, ), + ), + ], + ), floatingActionButton: isConnected ? FloatingActionButton( onPressed: () => _navigateToAddEdit(), diff --git a/Manager/pubspec.lock b/Manager/pubspec.lock index 22b2bfb..6814c6b 100644 --- a/Manager/pubspec.lock +++ b/Manager/pubspec.lock @@ -509,6 +509,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lpinyin: + dependency: "direct main" + description: + name: lpinyin + sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a" + url: "https://pub.dev" + source: hosted + version: "2.0.3" matcher: dependency: transitive description: diff --git a/Manager/pubspec.yaml b/Manager/pubspec.yaml index e09c950..9ad42c8 100644 --- a/Manager/pubspec.yaml +++ b/Manager/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: http_server: ^1.0.0 image_picker: ^1.2.1 intl: ^0.20.2 + lpinyin: ^2.0.3 mobile_scanner: ^7.1.4 network_info_plus: ^7.0.0 path: ^1.9.1