import 'dart:io'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import '../models/sake_item.dart'; import '../services/gemini_service.dart'; import '../services/sake_recommendation_service.dart'; import '../widgets/analyzing_dialog.dart'; import '../widgets/sake_3d_carousel.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../providers/sake_list_provider.dart'; import '../widgets/sake_radar_chart.dart'; import '../services/pricing_calculator.dart'; import '../providers/theme_provider.dart'; import '../models/user_profile.dart'; import 'camera_screen.dart'; class SakeDetailScreen extends ConsumerStatefulWidget { final SakeItem sake; const SakeDetailScreen({super.key, required this.sake}); @override ConsumerState createState() => _SakeDetailScreenState(); } class _SakeDetailScreenState extends ConsumerState { // To trigger rebuilds if we don't switch to a stream late SakeItem _sake; int _currentImageIndex = 0; @override void initState() { super.initState(); _sake = widget.sake; } @override Widget build(BuildContext context) { // Determine confidence text color final score = _sake.metadata.aiConfidence ?? 0; final Color confidenceColor = score > 80 ? Colors.green : score > 50 ? Colors.orange : Colors.red; // スマートレコメンド (Phase 1-8 Enhanced) final allSakeAsync = ref.watch(rawSakeListItemsProvider); final allSake = allSakeAsync.asData?.value ?? []; // 新しいレコメンドエンジン使用(五味チャート類似度込み) final recommendations = SakeRecommendationService.getRecommendations( target: _sake, allItems: allSake, limit: 10, ); final relatedItems = recommendations.map((rec) => rec.item).toList(); return Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 400.0, floating: false, pinned: true, iconTheme: const IconThemeData(color: Colors.white), actions: [ IconButton( icon: Icon(_sake.userData.isFavorite ? Icons.favorite : Icons.favorite_border), color: _sake.userData.isFavorite ? Colors.pink : Colors.white, tooltip: 'お気に入り', onPressed: () => _toggleFavorite(), ), IconButton( icon: const Icon(LucideIcons.refreshCw), color: Colors.white, tooltip: 'AI再解析', onPressed: () => _reanalyze(context), ), IconButton( icon: const Icon(LucideIcons.trash2), color: Colors.white, tooltip: '削除', onPressed: () => _showDeleteDialog(context), ), ], flexibleSpace: FlexibleSpaceBar( background: Stack( fit: StackFit.expand, children: [ _sake.displayData.imagePaths.length > 1 ? Stack( fit: StackFit.expand, children: [ PageView.builder( itemCount: _sake.displayData.imagePaths.length, onPageChanged: (index) => setState(() => _currentImageIndex = index), itemBuilder: (context, index) { return Image.file( File(_sake.displayData.imagePaths[index]), fit: BoxFit.cover, ); }, ), // Simple Indicator Positioned( bottom: 16, right: 16, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(16), ), child: Text( '${_currentImageIndex + 1} / ${_sake.displayData.imagePaths.length}', style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), ), ), ), // Photo Edit Button Positioned( bottom: 16, left: 16, child: FloatingActionButton.small( heroTag: 'photo_edit', backgroundColor: Colors.white, onPressed: () => _showPhotoEditModal(context), child: const Icon(LucideIcons.image, color: Colors.black87), ), ), ], ) : Stack( fit: StackFit.expand, children: [ Hero( tag: _sake.id, child: _sake.displayData.imagePaths.isNotEmpty ? Image.file( File(_sake.displayData.imagePaths.first), fit: BoxFit.cover, ) : Container( color: Colors.grey[300], child: const Icon(LucideIcons.image, size: 80, color: Colors.grey), ), ), // Photo Edit Button for single image Positioned( bottom: 16, left: 16, child: FloatingActionButton.small( heroTag: 'photo_edit_single', backgroundColor: Colors.white, onPressed: () => _showPhotoEditModal(context), child: const Icon(LucideIcons.image, color: Colors.black87), ), ), ], ), // Scrim for Header Icons Visibility Positioned( top: 0, left: 0, right: 0, height: 100, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.7), Colors.transparent, ], ), ), ), ), ], ), ), ), SliverToBoxAdapter( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Theme.of(context).scaffoldBackgroundColor, Theme.of(context).primaryColor.withValues(alpha: 0.05), ], ), ), padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Confidence Badge if (_sake.metadata.aiConfidence != null && _sake.itemType != ItemType.set) Center( child: Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: confidenceColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: confidenceColor.withValues(alpha: 0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(LucideIcons.sparkles, size: 14, color: confidenceColor), const SizedBox(width: 6), Text( 'AI確信度: $score%', style: TextStyle( color: confidenceColor, fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), ), // Brand Name Center( child: InkWell( onTap: () => _showTextEditDialog( context, title: '銘柄名を編集', initialValue: _sake.displayData.name, onSave: (value) async { final box = Hive.box('sake_items'); final updated = _sake.copyWith(name: value, isUserEdited: true); await box.put(_sake.key, updated); setState(() => _sake = updated); }, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( _sake.displayData.name, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, letterSpacing: 1.2, ), textAlign: TextAlign.center, ), ), const SizedBox(width: 8), Icon(LucideIcons.edit3, size: 20, color: Colors.grey[600]), ], ), ), ), const SizedBox(height: 8), // Brand / Prefecture if (_sake.itemType != ItemType.set) Center( child: InkWell( onTap: () => _showBreweryEditDialog(context), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( '${_sake.displayData.brewery} / ${_sake.displayData.prefecture}', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[600], fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 8), Icon(LucideIcons.edit3, size: 18, color: Colors.grey[500]), ], ), ), ), const SizedBox(height: 16), // Tags Row if (_sake.hiddenSpecs.flavorTags.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showTagEditDialog(context), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(8.0), child: Wrap( alignment: WrapAlignment.center, spacing: 8, children: _sake.hiddenSpecs.flavorTags.map((tag) => Chip( label: Text(tag, style: const TextStyle(fontSize: 10)), visualDensity: VisualDensity.compact, backgroundColor: Theme.of(context).primaryColor.withValues(alpha: 0.1), )).toList(), ), ), ), ), ), const SizedBox(height: 24), // AI Catchcopy (Mincho) if (_sake.displayData.catchCopy != null && _sake.itemType != ItemType.set) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( _sake.displayData.catchCopy!, style: GoogleFonts.zenOldMincho( fontSize: 24, height: 1.5, fontWeight: FontWeight.w500, color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Theme.of(context).primaryColor, // Adaptive ), textAlign: TextAlign.center, ), ), const SizedBox(height: 32), const SizedBox(height: 24), const Divider(), const SizedBox(height: 24), // Taste Radar Chart (Phase 1-8) if (_sake.hiddenSpecs.tasteStats.isNotEmpty && _sake.itemType != ItemType.set) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ Row( children: [ Icon(LucideIcons.barChart2, size: 16, color: Theme.of(context).colorScheme.onSurface), // Adaptive Color const SizedBox(width: 8), Text( 'Visual Tasting', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface, // Adaptive Color fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), SizedBox( height: 200, child: SakeRadarChart( tasteStats: _sake.hiddenSpecs.tasteStats, primaryColor: Theme.of(context).primaryColor, ), ), ], ), ), const SizedBox(height: 24), const Divider(), const SizedBox(height: 24), // Description if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set) Text( _sake.hiddenSpecs.description!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( height: 1.8, fontSize: 16, ), ), const SizedBox(height: 32), const Divider(), const SizedBox(height: 16), // AI Specs Accordion if (_sake.itemType != ItemType.set) ExpansionTile( leading: Icon(LucideIcons.sparkles, color: Theme.of(context).primaryColor), title: const Text('AIで分析された情報', style: TextStyle(fontWeight: FontWeight.bold)), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( children: [ _buildSpecRow('甘辛度', _sake.hiddenSpecs.sweetnessScore?.toStringAsFixed(1) ?? '-'), _buildSpecRow('濃淡度', _sake.hiddenSpecs.bodyScore?.toStringAsFixed(1) ?? '-'), const SizedBox(height: 8), Text( '※ 今後のアップデートで精米歩合、アルコール度数などの詳細スペックを追加予定', style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], ), ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), // Memo Field Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(LucideIcons.fileText, size: 16, color: Theme.of(context).primaryColor), const SizedBox(width: 8), Text( 'メモ', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 12), TextField( controller: TextEditingController(text: _sake.userData.memo ?? ''), maxLines: 4, decoration: InputDecoration( hintText: 'お店独自の情報をメモ', border: const OutlineInputBorder(), filled: true, fillColor: Theme.of(context).cardColor, ), onChanged: (value) async { // Auto-save final box = Hive.box('sake_items'); final updated = _sake.copyWith(memo: value, isUserEdited: true); await box.put(_sake.key, updated); setState(() => _sake = updated); }, ), ], ), const SizedBox(height: 48), // Related Items 3D Carousel (Phase 1-8 Enhanced) if (_sake.itemType != ItemType.set) ...[ // Hide for Set Items Row( children: [ Icon(LucideIcons.sparkles, size: 16, color: Theme.of(context).colorScheme.onSurface), const SizedBox(width: 8), Text( 'あわせて飲みたい', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface, // Adaptive ), ), ], ), const SizedBox(height: 8), Text( '五味チャート・タグ・酒蔵・産地から自動選出', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[600], ), ), const SizedBox(height: 16), relatedItems.isNotEmpty ? Sake3DCarousel( items: relatedItems, height: 220, ) : Container( height: 120, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.info, color: Colors.grey[400], size: 32), const SizedBox(height: 8), Text( '関連する日本酒を追加すると\nおすすめが表示されます', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], ), ), const SizedBox(height: 48), ], // Diagnostic Placeholder (Phase 1-6) Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( border: Border.all( color: Theme.of(context).primaryColor.withValues(alpha: 0.3), style: BorderStyle.solid, width: 2, ), borderRadius: BorderRadius.circular(16), color: Theme.of(context).cardColor.withValues(alpha: 0.5), ), child: Column( children: [ Icon(LucideIcons.wand2, color: Theme.of(context).colorScheme.onSurface, size: 32), const SizedBox(height: 12), Text( '診断スタンプ (Coming Soon)', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( 'MBTI診断との相性がここに表示されます', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey), ), ], ), ), const SizedBox(height: 24), ], ), ), ), // Phase 2-3: Business Pricing Section SliverToBoxAdapter( child: Consumer( builder: (context, ref, _) { final userProfile = ref.watch(userProfileProvider); if (!userProfile.isBusinessMode) return const SizedBox.shrink(); return _buildPricingSection(context, userProfile); }, ), ), // End of Pricing Section // Gap with Safe Area SliverPadding( padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), ), ], ), ); } bool _isAnalyzing = false; DateTime? _quotaLockoutTime; Future _toggleFavorite() async { final box = Hive.box('sake_items'); final newItem = _sake.copyWith(isFavorite: !_sake.userData.isFavorite); await box.put(_sake.key, newItem); setState(() { _sake = newItem; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(newItem.userData.isFavorite ? 'お気に入りに追加しました' : 'お気に入りを解除しました'), duration: const Duration(milliseconds: 1000), ), ); } Future _reanalyze(BuildContext context) async { // 1. Check Locks if (_isAnalyzing) return; // 2. Check Quota Lockout if (_quotaLockoutTime != null) { final remaining = _quotaLockoutTime!.difference(DateTime.now()); if (remaining.isNegative) { setState(() => _quotaLockoutTime = null); // Reset if time passed } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。')), ); return; } } if (_sake.displayData.imagePaths.isEmpty) return; setState(() => _isAnalyzing = true); try { showDialog( context: context, barrierDismissible: false, builder: (context) => const AnalyzingDialog(), ); final geminiService = GeminiService(); final result = await geminiService.analyzeSakeLabel(_sake.displayData.imagePaths); final newItem = _sake.copyWith( name: result.name ?? _sake.displayData.name, brand: result.brand ?? _sake.displayData.brewery, prefecture: result.prefecture ?? _sake.displayData.prefecture, description: result.description ?? _sake.hiddenSpecs.description, catchCopy: result.catchCopy ?? _sake.displayData.catchCopy, confidenceScore: result.confidenceScore, flavorTags: result.flavorTags.isNotEmpty ? result.flavorTags : _sake.hiddenSpecs.flavorTags, tasteStats: result.tasteStats.isNotEmpty ? result.tasteStats : _sake.hiddenSpecs.tasteStats, ); final box = Hive.box('sake_items'); await box.put(_sake.key, newItem); setState(() { _sake = newItem; }); if (context.mounted) { Navigator.pop(context); // Close dialog ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('再解析が完了しました')), ); } } catch (e) { if (context.mounted) { Navigator.pop(context); // Close dialog (safely pops the top route, hopefully the dialog) // Check for Quota Error to set Lockout if (e.toString().contains('Quota') || e.toString().contains('429')) { setState(() { _quotaLockoutTime = DateTime.now().add(const Duration(minutes: 1)); }); } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('エラー: $e')), ); } } finally { if (mounted) { setState(() => _isAnalyzing = false); } } } void _showTagEditDialog(BuildContext context) { final TextEditingController tagController = TextEditingController(); final allTags = _sake.hiddenSpecs.flavorTags.toSet(); showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (context, setModalState) { return AlertDialog( title: const Text('タグ編集'), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.85, minWidth: 300, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, children: allTags.map((tag) => Chip( label: Text(tag), onDeleted: () { setModalState(() => allTags.remove(tag)); }, )).toList(), ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: tagController, decoration: const InputDecoration( hintText: '新しいタグを追加', isDense: true, ), onSubmitted: (val) { if (val.trim().isNotEmpty) { setModalState(() { allTags.add(val.trim()); tagController.clear(); }); } }, ), ), IconButton( icon: const Icon(LucideIcons.plus), onPressed: () { if (tagController.text.trim().isNotEmpty) { setModalState(() { allTags.add(tagController.text.trim()); tagController.clear(); }); } }, ), ], ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), TextButton( onPressed: () { _updateTags(allTags.toList()); Navigator.pop(context); }, child: const Text('保存'), ), ], ); } ), ); } Future _updateTags(List newTags) async { final box = Hive.box('sake_items'); final newItem = _sake.copyWith( flavorTags: newTags, isUserEdited: true, ); await box.put(_sake.key, newItem); setState(() => _sake = newItem); } // Phase 2-3: Business Pricing UI (Simplified) Widget _buildPricingSection(BuildContext context, UserProfile userProfile) { // Calculated Price final calculatedPrice = PricingCalculator.calculatePrice(_sake); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(LucideIcons.coins, color: Colors.orange[800], size: 18), const SizedBox(width: 6), Text( '価格設定', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.orange[900]), ), ], ), const SizedBox(height: 4), Text( calculatedPrice > 0 ? '現在$calculatedPrice円' : '未設定', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: calculatedPrice > 0 ? Colors.orange[900] : Colors.grey, ), ), ], ), ElevatedButton( onPressed: () => _showPriceSettingsDialog(userProfile), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, ), child: const Text('編集'), ), ], ), ); } Future _showPriceSettingsDialog(UserProfile argProfile) async { // final userProfile = ref.read(userProfileProvider); // Now passed as arg if (!argProfile.isBusinessMode) return; int? cost = _sake.userData.costPrice; int? manual = _sake.userData.price; double markup = _sake.userData.markup; // Copy existing variants Map variants = Map.from(_sake.userData.priceVariants ?? {}); // Tiny State for Inline Adding String tempName = ''; String tempPrice = ''; // String to handle empty better final TextEditingController nameController = TextEditingController(); final TextEditingController priceController = TextEditingController(); await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setModalState) { final price = PricingCalculator.calculatePrice( _sake.copyWith(costPrice: cost, manualPrice: manual, markup: markup) ); void addVariant() { if (tempName.isNotEmpty && tempPrice.isNotEmpty) { final parsedPrice = int.tryParse(tempPrice); if (parsedPrice != null) { setModalState(() { variants[tempName] = parsedPrice; // Clear inputs tempName = ''; tempPrice = ''; nameController.clear(); priceController.clear(); }); } } } return AlertDialog( title: const Text('価格設定', style: TextStyle(fontWeight: FontWeight.bold)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 1. Manual Price (Top Priority) TextFormField( initialValue: manual?.toString() ?? '', keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '販売価格 (税込)', hintText: '手動で設定する場合に入力', suffixText: '円', border: OutlineInputBorder(), ), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), onChanged: (v) => setModalState(() => manual = int.tryParse(v)), ), const SizedBox(height: 24), // 2. Variants (Inline Entry) const Text('提供バリエーション', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), // Presets Chips Wrap( spacing: 8.0, runSpacing: 4.0, children: [ for (var preset in ['グラス (90ml)', '一合 (180ml)', 'ボトル (720ml)']) ChoiceChip( label: Text(preset), selected: tempName == preset, onSelected: (selected) { setModalState(() { // Auto-fill logic if (selected) { tempName = preset; nameController.text = preset; } }); }, backgroundColor: Colors.grey[200], selectedColor: Colors.orange[100], ), ], ), const SizedBox(height: 12), // Inline Inputs Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, child: TextField( controller: nameController, decoration: const InputDecoration( labelText: '名称', hintText: '例: 徳利', isDense: true, border: OutlineInputBorder(), ), onChanged: (v) => tempName = v, ), ), const SizedBox(width: 8), Expanded( flex: 2, child: TextField( controller: priceController, // Using controller to clear keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '価格', suffixText: '円', isDense: true, border: OutlineInputBorder(), ), onChanged: (v) => tempPrice = v, ), ), ], ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon(LucideIcons.plus), label: const Text('リストに追加'), style: ElevatedButton.styleFrom( backgroundColor: (tempName.isNotEmpty && tempPrice.isNotEmpty) ? Colors.orange : Colors.grey, foregroundColor: Colors.white, ), onPressed: (tempName.isNotEmpty && tempPrice.isNotEmpty) ? addVariant : null, ), ), const SizedBox(height: 16), // List of Added Variants if (variants.isNotEmpty) Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ ...variants.entries.map((e) => Column( children: [ ListTile( dense: true, title: Text(e.key, style: const TextStyle(fontWeight: FontWeight.w500)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text('${PricingCalculator.formatPrice(e.value)}円', style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(width: 8), IconButton( icon: const Icon(LucideIcons.x, color: Colors.grey, size: 18), onPressed: () { setModalState(() { variants.remove(e.key); }); }, ), ], ), ), if (e.key != variants.keys.last) const Divider(height: 1), ], )), ], ), ), const SizedBox(height: 24), // 3. Auto Calculation (Accordion) ExpansionTile( title: const Text('原価・掛率設定 (自動計算)', style: TextStyle(fontSize: 14, color: Colors.grey)), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), child: Column( children: [ TextFormField( initialValue: cost?.toString() ?? '', keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '仕入れ値 (円)', suffixText: '円', border: OutlineInputBorder(), prefixIcon: Icon(Icons.currency_yen), ), onChanged: (v) => setModalState(() => cost = int.tryParse(v)), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('掛率: ${markup.toStringAsFixed(1)}倍'), TextButton( child: const Text('リセット'), onPressed: () => setModalState(() => markup = argProfile.defaultMarkup), ) ], ), Slider( value: markup, min: 1.0, max: 5.0, divisions: 40, label: markup.toStringAsFixed(1), activeColor: Colors.orange, onChanged: (v) => setModalState(() => markup = v), ), Text( '参考計算価格: ${PricingCalculator.formatPrice(PricingCalculator.roundUpTo50((cost ?? 0) * markup))}円 (50円切上)', style: const TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ), ], ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () { _updatePricing( costPrice: cost, manualPrice: manual, markup: markup, priceVariants: variants.isEmpty ? null : variants, ); Navigator.pop(context); }, child: const Text('保存'), ), ], ); } ), ); } Future _updatePricing({int? costPrice, int? manualPrice, double? markup, Map? priceVariants}) async { final box = Hive.box('sake_items'); final newItem = _sake.copyWith( costPrice: costPrice, manualPrice: manualPrice, markup: markup ?? _sake.userData.markup, priceVariants: priceVariants, isUserEdited: true, ); await box.put(_sake.key, newItem); setState(() => _sake = newItem); } Future _showDeleteDialog(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon(LucideIcons.alertTriangle, color: Colors.orange, size: 24), const SizedBox(width: 8), const Text('削除確認'), ], ), content: Text('「${_sake.displayData.name}」を削除しますか?\nこの操作は取り消せません。'), actions: [ TextButton( child: const Text('キャンセル'), onPressed: () => Navigator.pop(context, false), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red, // Keeps Red for delete as it is destructive foregroundColor: Colors.white, ), child: const Text('削除'), onPressed: () => Navigator.pop(context, true), ), ], ), ); if (confirmed == true && mounted) { final box = Hive.box('sake_items'); await box.delete(_sake.key); if (mounted) { Navigator.pop(context); // Return to previous screen ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('削除しました')), ); } } } /// スペック行を構築 Widget _buildSpecRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(fontWeight: FontWeight.w500)), Text(value, style: TextStyle(color: Colors.grey[700])), ], ), ); } /// テキスト編集ダイアログを表示 Future _showTextEditDialog( BuildContext context, { required String title, required String initialValue, required Future Function(String) onSave, }) async { final controller = TextEditingController(text: initialValue); await showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: TextField( controller: controller, decoration: const InputDecoration( border: OutlineInputBorder(), ), autofocus: true, maxLines: null, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () async { await onSave(controller.text); if (context.mounted) Navigator.pop(context); }, child: const Text('保存'), ), ], ), ); } /// 酒蔵・都道府県編集ダイアログを表示 Future _showBreweryEditDialog(BuildContext context) async { final breweryController = TextEditingController(text: _sake.displayData.brewery); final prefectureController = TextEditingController(text: _sake.displayData.prefecture); await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('酒蔵・都道府県を編集'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: breweryController, decoration: const InputDecoration( labelText: '酒蔵', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: prefectureController, decoration: const InputDecoration( labelText: '都道府県', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () async { final box = Hive.box('sake_items'); final updated = _sake.copyWith( brand: breweryController.text, prefecture: prefectureController.text, isUserEdited: true, ); await box.put(_sake.key, updated); setState(() => _sake = updated); if (context.mounted) Navigator.pop(context); }, child: const Text('保存'), ), ], ), ); } /// 写真編集モーダルを表示 Future _showPhotoEditModal(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _PhotoEditModal( sake: _sake, onUpdated: (updatedSake) { setState(() => _sake = updatedSake); }, ), ); } } /// 写真編集モーダルウィジェット class _PhotoEditModal extends StatefulWidget { final SakeItem sake; final Function(SakeItem) onUpdated; const _PhotoEditModal({ required this.sake, required this.onUpdated, }); @override State<_PhotoEditModal> createState() => _PhotoEditModalState(); } class _PhotoEditModalState extends State<_PhotoEditModal> { late List _imagePaths; @override void initState() { super.initState(); _imagePaths = List.from(widget.sake.displayData.imagePaths); } @override Widget build(BuildContext context) { return Container( height: MediaQuery.of(context).size.height * 0.7, decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ // Handle bar Container( margin: const EdgeInsets.only(top: 12, bottom: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), // Header Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '写真を編集', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context), ), ], ), ), const Divider(height: 1), // Photo grid Expanded( child: _imagePaths.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.image, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( '写真を追加してください', style: TextStyle(color: Colors.grey[600]), ), ], ), ) : ReorderableListView.builder( padding: const EdgeInsets.all(16), itemCount: _imagePaths.length, onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < newIndex) { newIndex -= 1; } final item = _imagePaths.removeAt(oldIndex); _imagePaths.insert(newIndex, item); }); }, itemBuilder: (context, index) { final path = _imagePaths[index]; return Card( key: ValueKey(path), margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(path), width: 60, height: 60, fit: BoxFit.cover, ), ), title: Text( index == 0 ? 'メイン写真' : '写真 ${index + 1}', style: const TextStyle(fontWeight: FontWeight.w500), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(LucideIcons.gripVertical, color: Colors.grey[400]), const SizedBox(width: 8), IconButton( icon: const Icon(LucideIcons.trash2, color: Colors.red), onPressed: () => _deletePhoto(index), ), ], ), ), ); }, ), ), // Bottom buttons Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).cardColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: Row( children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(LucideIcons.camera), label: const Text('写真を追加'), onPressed: _addPhoto, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: _saveChanges, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text('保存'), ), ), ], ), ), ), ], ), ); } Future _addPhoto() async { final picker = ImagePicker(); // Show bottom sheet with camera/gallery options final source = await showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Wrap( children: [ ListTile( leading: const Icon(LucideIcons.camera), title: const Text('カメラで撮影'), onTap: () async { Navigator.pop(context); // Close sheet // Navigate to CameraScreen in returnPath mode final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => CameraScreen(mode: CameraMode.returnPath)), ); if (result is String) { // Add the path await _saveNewPhoto(result); } }, ), ListTile( leading: const Icon(LucideIcons.image), title: const Text('ギャラリーから選択'), onTap: () => Navigator.pop(context, ImageSource.gallery), ), ], ), ), ); if (source == null) return; // Handle Gallery (Camera is handled in ListTile callback) if (source == ImageSource.gallery) { try { final XFile? pickedFile = await picker.pickImage(source: source); if (pickedFile == null) return; // Save to app directory final appDir = await getApplicationDocumentsDirectory(); final fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg'; final savedPath = path.join(appDir.path, fileName); await File(pickedFile.path).copy(savedPath); await _saveNewPhoto(savedPath); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('エラー: $e')), ); } } } } Future _saveNewPhoto(String imagePath) async { setState(() { _imagePaths.add(imagePath); }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('写真を追加しました')), ); } } void _deletePhoto(int index) { setState(() { _imagePaths.removeAt(index); }); } Future _saveChanges() async { final box = Hive.box('sake_items'); final updatedSake = widget.sake.copyWith( imagePaths: _imagePaths, isUserEdited: true, ); await box.put(widget.sake.key, updatedSake); widget.onUpdated(updatedSake); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('写真を更新しました')), ); } } }