import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:uuid/uuid.dart'; import '../models/purchase_entry_models.dart'; import '../models/supplier_model.dart'; import '../services/purchase_entry_service.dart'; import '../widgets/keyboard_inset_wrapper.dart'; import '../widgets/line_item_editor.dart'; import '../widgets/modal_utils.dart'; import '../widgets/screen_id_title.dart'; import 'product_picker_modal.dart'; import 'supplier_picker_modal.dart'; class PurchaseEntriesScreen extends StatefulWidget { const PurchaseEntriesScreen({super.key}); @override State createState() => _PurchaseEntriesScreenState(); } class _PurchaseEntriesScreenState extends State { final PurchaseEntryService _service = PurchaseEntryService(); final NumberFormat _currencyFormat = NumberFormat.currency(locale: 'ja_JP', symbol: '¥'); bool _isLoading = true; bool _isRefreshing = false; PurchaseEntryStatus? _filterStatus; List _entries = const []; @override void initState() { super.initState(); _loadEntries(); } Future _loadEntries() async { if (!_isRefreshing) { setState(() => _isLoading = true); } try { final entries = await _service.fetchEntries(status: _filterStatus); if (!mounted) return; setState(() { _entries = entries; _isLoading = false; _isRefreshing = false; }); } catch (e) { if (!mounted) return; setState(() { _isLoading = false; _isRefreshing = false; }); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('仕入伝票の取得に失敗しました: $e'))); } } void _setFilter(PurchaseEntryStatus? status) { setState(() => _filterStatus = status); _loadEntries(); } Future _handleRefresh() async { setState(() => _isRefreshing = true); await _loadEntries(); } Future _openEditor({PurchaseEntry? entry}) async { final saved = await Navigator.push( context, MaterialPageRoute(builder: (_) => PurchaseEntryEditorPage(entry: entry)), ); if (saved != null) { await _loadEntries(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入伝票を保存しました'))); } } Future _deleteEntry(PurchaseEntry entry) async { final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('仕入伝票を削除'), content: Text('${entry.subject ?? '無題'} を削除しますか?'), 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) return; await _service.deleteEntry(entry.id); if (!mounted) return; await _loadEntries(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入伝票を削除しました'))); } @override Widget build(BuildContext context) { final body = _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _handleRefresh, child: _entries.isEmpty ? ListView( children: const [ SizedBox(height: 140), Icon(Icons.receipt_long, size: 64, color: Colors.grey), SizedBox(height: 12), Center(child: Text('仕入伝票がありません。右下のボタンから登録してください。')), ], ) : ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 120), itemCount: _entries.length, itemBuilder: (context, index) => _buildEntryCard(_entries[index]), ), ); return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( leading: const BackButton(), title: const ScreenAppBarTitle(screenId: 'P1', title: '仕入伝票一覧'), actions: [ PopupMenuButton( icon: const Icon(Icons.filter_alt), onSelected: _setFilter, itemBuilder: (context) => [ const PopupMenuItem(value: null, child: Text('すべて')), ...PurchaseEntryStatus.values.map((status) => PopupMenuItem( value: status, child: Text(status.displayName), )), ], ), ], ), body: body, floatingActionButton: FloatingActionButton.extended( onPressed: () => _openEditor(), icon: const Icon(Icons.add), label: const Text('仕入伝票を登録'), ), ); } Widget _buildEntryCard(PurchaseEntry entry) { return Card( color: Colors.white, margin: const EdgeInsets.only(bottom: 12), child: ListTile( onTap: () => _openEditor(entry: entry), onLongPress: () => _deleteEntry(entry), title: Text(entry.subject?.isNotEmpty == true ? entry.subject! : '無題の仕入伝票', style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Padding( padding: const EdgeInsets.only(top: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(entry.supplierNameSnapshot ?? '仕入先未設定'), const SizedBox(height: 4), Text('計上日: ${DateFormat('yyyy/MM/dd').format(entry.issueDate)}'), ], ), ), trailing: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(entry.status.displayName, style: const TextStyle(fontSize: 12, color: Colors.black54)), const SizedBox(height: 4), Text( _currencyFormat.format(entry.amountTaxIncl), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ), ), ); } } class PurchaseEntryEditorPage extends StatefulWidget { const PurchaseEntryEditorPage({super.key, this.entry}); final PurchaseEntry? entry; @override State createState() => _PurchaseEntryEditorPageState(); } class _PurchaseEntryEditorPageState extends State { final PurchaseEntryService _service = PurchaseEntryService(); final TextEditingController _subjectController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); final uuid = const Uuid(); Supplier? _supplier; String? _supplierSnapshot; DateTime _issueDate = DateTime.now(); bool _isSaving = false; final List _lines = []; @override void initState() { super.initState(); final entry = widget.entry; if (entry != null) { _subjectController.text = entry.subject ?? ''; _notesController.text = entry.notes ?? ''; _issueDate = entry.issueDate; _supplierSnapshot = entry.supplierNameSnapshot; _lines.addAll(entry.items .map((item) => LineItemFormData( id: item.id, productId: item.productId, productName: item.description, quantity: item.quantity, unitPrice: item.unitPrice, taxRate: item.taxRate, )) .toList()); } if (_lines.isEmpty) { _lines.add(LineItemFormData(quantity: 1, unitPrice: 0)); } } @override void dispose() { _subjectController.dispose(); _notesController.dispose(); for (final line in _lines) { line.dispose(); } super.dispose(); } Future _pickSupplier() async { final selected = await showFeatureModalBottomSheet( context: context, builder: (ctx) => SupplierPickerModal( onSupplierSelected: (supplier) { Navigator.pop(ctx, supplier); }, ), ); if (selected == null) return; setState(() { _supplier = selected; _supplierSnapshot = selected.name; }); } Future _pickIssueDate() async { final picked = await showDatePicker( context: context, initialDate: _issueDate, firstDate: DateTime(2010), lastDate: DateTime(2100), ); if (picked == null) return; setState(() => _issueDate = picked); } void _addLine() { setState(() => _lines.add(LineItemFormData(quantity: 1, unitPrice: 0))); } void _removeLine(int index) { setState(() { final removed = _lines.removeAt(index); removed.dispose(); if (_lines.isEmpty) { _lines.add(LineItemFormData(quantity: 1, unitPrice: 0)); } }); } Future _pickProduct(int index) async { await showFeatureModalBottomSheet( context: context, builder: (_) => ProductPickerModal( onItemSelected: (_) {}, onProductSelected: (product) { setState(() => _lines[index].applyProduct(product)); }, ), ); } Future _save() async { if (_isSaving) return; if (_lines.every((line) => line.descriptionController.text.trim().isEmpty)) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('少なくとも1件の明細を入力してください'))); return; } setState(() => _isSaving = true); try { final now = DateTime.now(); final entryId = widget.entry?.id ?? uuid.v4(); final items = _lines.map((line) { final quantity = line.quantityValue; final unitPrice = line.unitPriceValue; return PurchaseLineItem( id: line.id ?? uuid.v4(), purchaseEntryId: entryId, description: line.description.isEmpty ? '商品' : line.description, quantity: quantity, unitPrice: unitPrice, lineTotal: quantity * unitPrice, productId: line.productId, taxRate: line.taxRate ?? 0, ); }).toList(); final entry = PurchaseEntry( id: entryId, supplierId: _supplier?.id ?? widget.entry?.supplierId, supplierNameSnapshot: _supplierSnapshot, subject: _subjectController.text.trim().isEmpty ? '仕入伝票' : _subjectController.text.trim(), issueDate: _issueDate, status: widget.entry?.status ?? PurchaseEntryStatus.draft, notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(), createdAt: widget.entry?.createdAt ?? now, updatedAt: now, items: items, ); final saved = await _service.saveEntry(entry); if (!mounted) return; Navigator.pop(context, saved); } catch (e) { if (!mounted) return; setState(() => _isSaving = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存に失敗しました: $e'))); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey.shade200, resizeToAvoidBottomInset: false, appBar: AppBar( leading: const BackButton(), title: ScreenAppBarTitle( screenId: 'P2', title: widget.entry == null ? '仕入伝票作成' : '仕入伝票編集', ), actions: [ TextButton(onPressed: _isSaving ? null : _save, child: const Text('保存')), ], ), body: KeyboardInsetWrapper( safeAreaTop: false, basePadding: const EdgeInsets.fromLTRB(16, 16, 16, 16), extraBottom: 32, child: SingleChildScrollView( padding: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( color: Colors.white, child: ListTile( title: Text(_supplierSnapshot ?? '仕入先を選択'), subtitle: const Text('タップして仕入先を選択'), trailing: const Icon(Icons.chevron_right), onTap: _pickSupplier, ), ), const SizedBox(height: 12), Card( color: Colors.white, child: ListTile( title: const Text('計上日'), subtitle: Text(DateFormat('yyyy/MM/dd').format(_issueDate)), trailing: TextButton(onPressed: _pickIssueDate, child: const Text('変更')), ), ), const SizedBox(height: 12), Card( color: Colors.white, child: Padding( padding: const EdgeInsets.all(12), child: TextField( controller: _subjectController, decoration: const InputDecoration(labelText: '件名'), ), ), ), const SizedBox(height: 20), Text('明細', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ..._lines.asMap().entries.map( (entry) => Padding( padding: const EdgeInsets.only(bottom: 8), child: LineItemCard( data: entry.value, onPickProduct: () => _pickProduct(entry.key), onRemove: () => _removeLine(entry.key), ), ), ), Align( alignment: Alignment.centerLeft, child: TextButton.icon(onPressed: _addLine, icon: const Icon(Icons.add), label: const Text('明細を追加')), ), const SizedBox(height: 20), Card( color: Colors.white, child: Padding( padding: const EdgeInsets.all(12), child: KeyboardInsetWrapper( basePadding: EdgeInsets.zero, safeAreaTop: false, safeAreaBottom: false, child: TextField( controller: _notesController, decoration: const InputDecoration(labelText: 'メモ'), minLines: 2, maxLines: 4, ), ), ), ), ], ), ), ), ); } }