import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; import '../models/product_model.dart'; import '../services/product_repository.dart'; import 'barcode_scanner_screen.dart'; class ProductMasterScreen extends StatefulWidget { final bool selectionMode; final bool showHidden; const ProductMasterScreen({super.key, this.selectionMode = false, this.showHidden = false}); @override State createState() => _ProductMasterScreenState(); } class _ProductMasterScreenState extends State { final ProductRepository _productRepo = ProductRepository(); final TextEditingController _searchController = TextEditingController(); List _products = []; List _filteredProducts = []; bool _isLoading = true; String _searchQuery = ""; @override void initState() { super.initState(); _loadProducts(); } Future _loadProducts() async { setState(() => _isLoading = true); final products = await _productRepo.getAllProducts(includeHidden: widget.showHidden); if (!mounted) return; setState(() { _products = products; _isLoading = false; _applyFilter(); }); } void _applyFilter() { setState(() { _filteredProducts = _products.where((p) { final query = _searchQuery.toLowerCase(); return p.name.toLowerCase().contains(query) || (p.barcode?.toLowerCase().contains(query) ?? false) || (p.category?.toLowerCase().contains(query) ?? false); }).toList(); if (!widget.showHidden) { _filteredProducts = _filteredProducts.where((p) => !p.isHidden).toList(); } if (widget.showHidden) { _filteredProducts.sort((a, b) => b.id.compareTo(a.id)); } }); } Future _showEditDialog({Product? product}) async { final nameController = TextEditingController(text: product?.name ?? ""); final priceController = TextEditingController(text: (product?.defaultUnitPrice ?? 0).toString()); final barcodeController = TextEditingController(text: product?.barcode ?? ""); final wholesaleController = TextEditingController(text: (product?.wholesalePrice ?? 0).toString()); final categoryController = TextEditingController(text: product?.category ?? ""); final stockController = TextEditingController(text: (product?.stockQuantity ?? 0).toString()); final result = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { final inset = MediaQuery.of(context).viewInsets.bottom; return MediaQuery.removeViewInsets( removeBottom: true, context: context, child: AlertDialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), title: Text(product == null ? "商品追加" : "商品編集"), content: SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: EdgeInsets.only(bottom: inset + 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: nameController, decoration: const InputDecoration(labelText: "商品名")), TextField(controller: categoryController, decoration: const InputDecoration(labelText: "カテゴリ")), TextField(controller: priceController, decoration: const InputDecoration(labelText: "初期単価"), keyboardType: TextInputType.number), TextField(controller: wholesaleController, decoration: const InputDecoration(labelText: "仕入値(卸値)"), keyboardType: TextInputType.number), TextField(controller: stockController, decoration: const InputDecoration(labelText: "在庫数"), keyboardType: TextInputType.number), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField(controller: barcodeController, decoration: const InputDecoration(labelText: "バーコード")), ), IconButton( icon: const Icon(Icons.qr_code_scanner), onPressed: () async { final code = await Navigator.push( context, MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()), ); if (code != null) { setDialogState(() => barcodeController.text = code); } }, ), ], ), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")), ElevatedButton( onPressed: () { if (nameController.text.isEmpty) return; final locked = product?.isLocked ?? false; final newId = locked ? const Uuid().v4() : (product?.id ?? const Uuid().v4()); Navigator.pop( context, Product( id: newId, name: nameController.text.trim(), defaultUnitPrice: int.tryParse(priceController.text) ?? 0, wholesalePrice: int.tryParse(wholesaleController.text) ?? 0, barcode: barcodeController.text.isEmpty ? null : barcodeController.text.trim(), category: categoryController.text.isEmpty ? null : categoryController.text.trim(), stockQuantity: int.tryParse(stockController.text) ?? 0, odooId: product?.odooId, isLocked: false, ), ); }, child: const Text("保存"), ), ], ), ); }, ), ); if (result != null) { if (!mounted) return; await _productRepo.saveProduct(result); _loadProducts(); } } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( leading: const BackButton(), title: const Text("P1:商品マスター"), backgroundColor: Colors.blueGrey, bottom: PreferredSize( preferredSize: const Size.fromHeight(60), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: "商品名・バーコード・カテゴリで検索", prefixIcon: const Icon(Icons.search), filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), contentPadding: EdgeInsets.zero, ), onChanged: (val) { _searchQuery = val; _applyFilter(); }, ), ), ), ), body: Padding( padding: const EdgeInsets.only(top: 8, bottom: 8), child: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredProducts.isEmpty ? const Center(child: Text("商品が見つかりません")) : ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(bottom: 80, top: 8), itemCount: _filteredProducts.length, itemBuilder: (context, index) { final p = _filteredProducts[index]; return ListTile( leading: CircleAvatar( backgroundColor: p.isLocked ? Colors.grey.shade300 : Colors.indigo.shade100, child: Stack( children: [ const Align(alignment: Alignment.center, child: Icon(Icons.inventory_2, color: Colors.indigo)), if (p.isLocked) const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)), ], ), ), title: Text( p.name + (p.isHidden ? " (非表示)" : ""), style: TextStyle( fontWeight: FontWeight.bold, color: p.isHidden ? Colors.grey : (p.isLocked ? Colors.grey : Colors.black87), ), ), subtitle: Text("${p.category ?? '未分類'} - 販売¥${p.defaultUnitPrice} / 仕入¥${p.wholesalePrice} (在庫: ${p.stockQuantity})"), onTap: () { if (widget.selectionMode) { if (p.isHidden) return; // safety: do not return hidden in selection Navigator.pop(context, p); } else { _showDetailPane(p); } }, onLongPress: () async { await showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.edit), title: const Text("編集"), onTap: () { Navigator.pop(ctx); _showEditDialog(product: p); }, ), if (!p.isHidden) ListTile( leading: const Icon(Icons.visibility_off), title: const Text("非表示にする"), onTap: () async { Navigator.pop(ctx); await _productRepo.setHidden(p.id, true); if (mounted) _loadProducts(); }, ), if (!p.isLocked) ListTile( leading: const Icon(Icons.delete_outline, color: Colors.redAccent), title: const Text("削除", style: TextStyle(color: Colors.redAccent)), onTap: () async { Navigator.pop(ctx); final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text("削除の確認"), content: Text("${p.name} を削除しますか?"), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")), TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))), ], ), ); if (confirmed == true) { await _productRepo.deleteProduct(p.id); if (mounted) _loadProducts(); } }, ), ], ), ), ); }, trailing: widget.selectionMode ? null : IconButton( icon: const Icon(Icons.edit), onPressed: p.isLocked ? null : () => _showEditDialog(product: p), tooltip: p.isLocked ? "ロック中" : "編集", ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () => _showEditDialog(), backgroundColor: Colors.blueGrey.shade800, foregroundColor: Colors.white, child: const Icon(Icons.add), ), ); } void _showDetailPane(Product p) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => DraggableScrollableSheet( initialChildSize: 0.45, maxChildSize: 0.8, minChildSize: 0.35, expand: false, builder: (context, scrollController) => Padding( padding: const EdgeInsets.all(16), child: ListView( controller: scrollController, children: [ Row( children: [ Icon(p.isLocked ? Icons.lock : Icons.inventory_2, color: p.isLocked ? Colors.redAccent : Colors.indigo), const SizedBox(width: 8), Expanded(child: Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), Chip(label: Text(p.category ?? '未分類')), ], ), const SizedBox(height: 8), Text("販売単価: ¥${p.defaultUnitPrice}"), Text("仕入値: ¥${p.wholesalePrice}"), Text("在庫: ${p.stockQuantity}"), if (p.barcode != null && p.barcode!.isNotEmpty) Text("バーコード: ${p.barcode}"), const SizedBox(height: 12), Row( children: [ OutlinedButton.icon( icon: const Icon(Icons.edit), label: const Text("編集"), onPressed: () { Navigator.pop(context); _showEditDialog(product: p); }, ), const SizedBox(width: 8), if (!p.isLocked) OutlinedButton.icon( icon: const Icon(Icons.delete_outline, color: Colors.redAccent), label: const Text("削除", style: TextStyle(color: Colors.redAccent)), onPressed: () async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("削除の確認"), content: Text("${p.name}を削除してよろしいですか?"), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red)), ), ], ), ); if (!context.mounted) return; if (confirmed == true) { await _productRepo.deleteProduct(p.id); if (!context.mounted) return; Navigator.pop(context); // sheet _loadProducts(); } }, ), if (p.isLocked) Padding( padding: const EdgeInsets.only(left: 8), child: Chip(label: const Text("ロック中"), avatar: const Icon(Icons.lock, size: 16)), ), ], ), ], ), ), ), ); } }