// Version: 1.0.0 import 'package:flutter/material.dart'; import '../services/database_helper.dart'; import '../models/product.dart'; /// 請求作成画面(見積フローから連携) class InvoiceScreen extends StatefulWidget { const InvoiceScreen({super.key}); @override State createState() => _InvoiceScreenState(); } class _InvoiceScreenState extends State { Customer? _selectedCustomer; final DatabaseHelper _db = DatabaseHelper.instance; List _products = []; List _customers = []; List _items = []; String _estimateNumber = ''; // 見積番号(参考用) DateTime _invoiceDate = DateTime.now(); @override void initState() { super.initState(); _loadProducts(); _loadCustomers(); } Future _loadProducts() async { try { final products = await _db.getProducts(); setState(() => _products = products); } catch (e) { debugPrint('Product loading failed: $e'); } } Future _loadCustomers() async { try { final customers = await _db.getCustomers(); setState(() => _customers = customers.where((c) => c.isDeleted == 0).toList()); } catch (e) { debugPrint('Customer loading failed: $e'); } } Future _showCustomerPicker() async { if (_customers.isEmpty) await _loadCustomers(); final selected = await showModalBottomSheet( context: context, builder: (ctx) => SizedBox( height: MediaQuery.of(context).size.height * 0.4, child: ListView.builder( padding: const EdgeInsets.all(8), itemCount: _customers.length, itemBuilder: (ctx, index) => ListTile( title: Text(_customers[index].name), subtitle: Text('コード:${_customers[index].customerCode}'), onTap: () => Navigator.pop(ctx, _customers[index]), ), ), ), ); if (selected is Customer && selected.id != _selectedCustomer?.id) { setState(() => _selectedCustomer = selected); } } void _addSelectedProducts() async { for (final product in _products) { final existingStock = product.stock; if (existingStock > 0 && !_items.any((i) => i.productId == product.id)) { setState(() => _items.add(LineItem( invoiceId: DateTime.now().millisecondsSinceEpoch, estimateNumber: 'EST${DateTime.now().year}${DateTime.now().month.toString().padLeft(2, '0')}', productId: product.id, productName: product.name, unitPrice: product.price, quantity: 1, total: product.price, ))); } } _showAddDialog(); } void _removeLineItem(int index) { setState(() => _items.removeAt(index)); } String get _invoiceId => _items.isNotEmpty ? 'INV${_items.first.invoiceId.toString()}' : ''; int get _totalAmount => _items.fold(0, (sum, item) => sum + item.total); int get _taxRate => _selectedCustomer?.taxRate ?? 8; // Default 10% int get _discountRate => _selectedCustomer?.discountRate ?? 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('請求作成')), body: ListView( padding: const EdgeInsets.all(16), children: [ _buildCustomerField(), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('請求日'), InkWell( onTap: () => _selectDate(), child: Text('${_invoiceDate.year}-${_invoiceDate.month.toString().padLeft(2, '0')}-${_invoiceDate.day.toString().padLeft(2, '0')}'), ), ], ), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: ExpansionTile( title: const Text('請求商品'), children: [ if (_items.isEmpty) ...[ Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Icon(Icons.receipt_long_outlined, size: 48, color: Colors.grey.shade400), const SizedBox(height: 8), Text('商品を追加してください', style: TextStyle(color: Colors.grey.shade600)), ], ), ), ] else ...[ ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, itemCount: _items.length, itemBuilder: (context, index) => Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: Colors.purple.shade100, child: Icon(Icons.receipt_long, color: Colors.purple), ), title: Text(_items[index].productName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('単価:¥${_items[index].unitPrice}'), Text('数量:${_items[index].quantity} pcs'), ], ), trailing: IconButton(icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _removeLineItem(index)), ), ), ), ], ), ), ), ], ), ); } Widget _buildCustomerField() { return TextField( decoration: InputDecoration( labelText: '得意先', hintText: _selectedCustomer != null ? _selectedCustomer.name : '得意先マスタから選択', prefixIcon: Icon(Icons.person_search), isReadOnly: true, ), onTap: () => _showCustomerPicker(), ); } void _selectDate() async { final picked = await showDatePicker( context: context, initialDate: _invoiceDate, firstDate: DateTime(2026), lastDate: DateTime(2100), ); if (picked != null) setState(() => _invoiceDate = picked); } void _showAddDialog() async { final selected = await showModalBottomSheet( context: context, builder: (ctx) => ListView.builder( padding: EdgeInsets.zero, itemCount: _products.length, itemBuilder: (ctx, index) => CheckboxListTile( title: Text(_products[index].name), subtitle: Text('¥${_products[index].price} / 在庫:${_products[index].stock}${_products[index].unit ?? ''}'), value: _items.any((i) => i.productId == _products[index].id), onChanged: (value) { if (value && _products[index].stock > 0) _addSelectedProducts(); }, ), ), ); if (selected != null && selected.id != null && _products[selected.id]?.stock! > 0 && !_items.any((i) => i.productId == selected.id)) { setState(() => _items.add(LineItem( invoiceId: _invoiceId, estimateNumber: 'EST${DateTime.now().year}${DateTime.now().month.toString().padLeft(2, '0')}', productId: selected.id, productName: selected.name, unitPrice: selected.price, quantity: 1, total: selected.price, ))); } } void _showSaveDialog() async { if (_items.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('商品を追加してください')), ); return; } showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('請求書発行'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_selectedCustomer != null) ...[ Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text('得意先:${_selectedCustomer!.name}'), ), ], Text('請求 ID: ${_items.isNotEmpty ? _invoiceId : ''}'), Text('見積番号:${_estimateNumber.isEmpty ? '(新規作成)' : _estimateNumber}'), Text('請求日:${_invoiceDate.toLocal()}'), Text('税率:${_taxRate}% / 割引率:${_discountRate}%'), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('合計金額(税込)', style: TextStyle(fontWeight: FontWeight.bold)), Text('¥${_totalAmount}'), ], ), if (_items.isNotEmpty) ...[ Divider(), ..._items.map((item) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(item.productName), Text('¥${item.unitPrice} × ${item.quantity} = ¥${item.total}'), ], ), )), ], ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'), ), ElevatedButton( onPressed: () async { if (_estimateNumber.isEmpty) { _estimateNumber = 'EST${DateTime.now().year}${DateTime.now().month.toString().padLeft(2, '0')}'; } Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('請求書発行しました'))..behavior: SnackBarBehavior.floating, ); }, style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), child: const Text('発行'), ), ], ), ); } void _saveInvoice() async { if (_items.isEmpty) return; // TODO: DB に請求書データを保存 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('請求書 ${_invoiceId} をデータベースに保存')), ); } String _formatAmount(int amount) { const symbol = '¥'; final integerPart = amount ~/ 100; // 円部分 final centPart = amount % 100; // 銭部分 return '$symbol${integerPart.toString().padLeft(2, '0')}.${centPart.toString().padLeft(2, '0')}'; } } /// 請求行モデル(見積番号参照) class LineItem { final String? invoiceId; final String estimateNumber; // 見積番号(関連付け用) final int? productId; final String productName; final int unitPrice; int quantity = 1; int get total => quantity * unitPrice; LineItem({this.invoiceId, this.estimateNumber = '', this.productId, required this.productName, required this.unitPrice, this.quantity = 1}); }