// lib/screens/invoice_detail_page.dart // version: 1.2.2 (Fix: Resolved Class name conflict strictly) import 'dart:io'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:share_plus/share_plus.dart'; import 'package:open_filex/open_filex.dart'; import 'package:printing/printing.dart'; import 'package:drift/drift.dart' as drift; import '../models/invoice_models.dart'; // 本来の Invoice, InvoiceItem を使用 import '../services/pdf_generator.dart'; import '../data/database.dart' as db; // エイリアスを db に固定 import '../main.dart'; import 'product_picker_modal.dart'; class InvoiceDetailPage extends StatefulWidget { final Invoice invoice; const InvoiceDetailPage({Key? key, required this.invoice}) : super(key: key); @override State createState() => _InvoiceDetailPageState(); } class _InvoiceDetailPageState extends State { late TextEditingController _formalNameController; late TextEditingController _notesController; late List _items; late bool _isEditing; late Invoice _currentInvoice; String? _currentFilePath; @override void initState() { super.initState(); _currentInvoice = widget.invoice; _currentFilePath = widget.invoice.filePath; _formalNameController = TextEditingController(text: _currentInvoice.customer.formalName); _notesController = TextEditingController(text: _currentInvoice.notes ?? ""); _items = List.from(_currentInvoice.items); _isEditing = false; } @override void dispose() { _formalNameController.dispose(); _notesController.dispose(); super.dispose(); } void _addItem() => setState(() => _items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0))); void _removeItem(int index) => setState(() => _items.removeAt(index)); void _pickFromMaster() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => FractionallySizedBox( heightFactor: 0.9, child: ProductPickerModal( onItemSelected: (item) { setState(() => _items.add(item)); Navigator.pop(context); }, ), ), ); } Future _saveChanges() async { final String formalName = _formalNameController.text.trim(); if (formalName.isEmpty) return; final updatedCustomer = _currentInvoice.customer.copyWith(formalName: formalName); final updatedInvoice = _currentInvoice.copyWith( customer: updatedCustomer, items: _items, notes: _notesController.text, ); setState(() => _isEditing = false); try { final File pdfFile = await PdfGenerator.generateInvoicePdf(updatedInvoice); final String newPath = pdfFile.path; // DB側のクラスにはすべて `db.` を付ける await database.into(database.customers).insertOnConflictUpdate( db.CustomersCompanion.insert( id: updatedCustomer.id, displayName: updatedCustomer.displayName, formalName: updatedCustomer.formalName, address: drift.Value(updatedCustomer.address), department: drift.Value(updatedCustomer.department), lastUpdatedAt: drift.Value(DateTime.now()), ), ); final invoiceCompanion = db.InvoicesCompanion.insert( id: updatedInvoice.invoiceNumber, customerId: updatedCustomer.id, date: updatedInvoice.date, type: "請求", filePath: drift.Value(newPath), notes: drift.Value(updatedInvoice.notes), totalAmount: updatedInvoice.totalAmount, ); // item の型を InvoiceItem と明示して getter エラーを回避 final List itemCompanions = _items.map((InvoiceItem item) => db.InvoiceItemsCompanion.insert( invoiceId: updatedInvoice.invoiceNumber, description: item.description, quantity: item.quantity, unitPrice: item.unitPrice, )).toList(); await database.saveFullInvoice(invoiceCompanion, itemCompanions); setState(() { _currentInvoice = updatedInvoice.copyWith(filePath: newPath); _currentFilePath = newPath; }); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存完了'))); await Printing.layoutPdf(onLayout: (format) async => pdfFile.readAsBytesSync(), name: newPath.split('/').last); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('エラー: $e'), backgroundColor: Colors.red)); } } @override Widget build(BuildContext context) { final amountFormatter = NumberFormat("#,###"); return Scaffold( appBar: AppBar( title: const Text("請求書詳細"), backgroundColor: Colors.blueGrey, foregroundColor: Colors.white, actions: [ if (!_isEditing) IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)) else ...[ IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges), IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)), ] ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeaderSection(), const Divider(height: 32), _buildItemTable(amountFormatter), if (_isEditing) _buildEditButtons(), const SizedBox(height: 24), _buildSummarySection(amountFormatter), const SizedBox(height: 24), _buildFooterActions(), ], ), ), ); } Widget _buildHeaderSection() { final dateFormatter = DateFormat('yyyy年MM月dd日'); if (_isEditing) { return Column( children: [ TextField(controller: _formalNameController, decoration: const InputDecoration(labelText: "取引先 正式名称")), const SizedBox(height: 12), TextField(controller: _notesController, maxLines: 2, decoration: const InputDecoration(labelText: "備考")), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${_currentInvoice.customer.formalName} 御中", style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), Text("請求番号: ${_currentInvoice.invoiceNumber}"), Text("発行日: ${dateFormatter.format(_currentInvoice.date)}"), ], ); } Widget _buildItemTable(NumberFormat formatter) { return Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const {0: FlexColumnWidth(4), 1: FixedColumnWidth(50), 2: FixedColumnWidth(80), 3: FlexColumnWidth(2), 4: FixedColumnWidth(40)}, children: [ TableRow(decoration: BoxDecoration(color: Colors.grey.shade100), children: const [_TableCell("品名"), _TableCell("数量"), _TableCell("単価"), _TableCell("金額"), _TableCell("")]), ..._items.asMap().entries.map((entry) { int idx = entry.key; InvoiceItem item = entry.value; if (_isEditing) { return TableRow(children: [ _EditableCell(initialValue: item.description, onChanged: (val) => item.description = val), _EditableCell(initialValue: item.quantity.toString(), keyboardType: TextInputType.number, onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0)), _EditableCell(initialValue: item.unitPrice.toString(), keyboardType: TextInputType.number, onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0)), _TableCell(formatter.format(item.subtotal)), IconButton(icon: const Icon(Icons.delete, color: Colors.red, size: 20), onPressed: () => _removeItem(idx)), ]); } return TableRow(children: [_TableCell(item.description), _TableCell(item.quantity.toString()), _TableCell(formatter.format(item.unitPrice)), _TableCell(formatter.format(item.subtotal)), const SizedBox()]); }), ], ); } Widget _buildEditButtons() => Row(children: [ElevatedButton(onPressed: _addItem, child: const Text("行追加")), const SizedBox(width: 8), ElevatedButton(onPressed: _pickFromMaster, child: const Text("マスター"))]); Widget _buildSummarySection(NumberFormat formatter) { // 確実に int で計算 int subtotal = _items.fold(0, (int sum, InvoiceItem item) => sum + (item.quantity * item.unitPrice)); return Align( alignment: Alignment.centerRight, child: Column(children: [Text("合計: ¥${formatter.format((subtotal * 1.1).floor())}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))]), ); } Widget _buildFooterActions() => (_isEditing || _currentFilePath == null) ? const SizedBox() : Row(children: [Expanded(child: ElevatedButton(onPressed: () => OpenFilex.open(_currentFilePath!), child: const Text("開く"))), const SizedBox(width: 12), Expanded(child: ElevatedButton(onPressed: () => Share.shareXFiles([XFile(_currentFilePath!)]), child: const Text("共有")))]); } class _TableCell extends StatelessWidget { final String text; const _TableCell(this.text); @override Widget build(BuildContext context) => Padding(padding: const EdgeInsets.all(8.0), child: Text(text, textAlign: TextAlign.right, style: const TextStyle(fontSize: 12))); } class _EditableCell extends StatelessWidget { final String initialValue; final TextInputType keyboardType; final Function(String) onChanged; const _EditableCell({required this.initialValue, this.keyboardType = TextInputType.text, required this.onChanged}); @override Widget build(BuildContext context) => Padding(padding: const EdgeInsets.symmetric(horizontal: 4), child: TextField(controller: TextEditingController(text: initialValue), keyboardType: keyboardType, style: const TextStyle(fontSize: 12), onChanged: onChanged)); }