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 '../models/invoice_models.dart'; import '../services/pdf_generator.dart'; import '../services/invoice_repository.dart'; import '../services/customer_repository.dart'; import '../services/company_repository.dart'; import 'product_picker_modal.dart'; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; import '../services/print_service.dart'; import '../models/company_model.dart'; class InvoiceDetailPage extends StatefulWidget { final Invoice invoice; final bool isUnlocked; const InvoiceDetailPage({Key? key, required this.invoice, this.isUnlocked = false}) : 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; final _invoiceRepo = InvoiceRepository(); final _customerRepo = CustomerRepository(); final _companyRepo = CompanyRepository(); CompanyInfo? _companyInfo; @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; _loadCompanyInfo(); } Future _loadCompanyInfo() async { final info = await _companyRepo.getCompanyInfo(); setState(() => _companyInfo = info); } @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) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('取引先の正式名称を入力してください')), ); return; } // 顧客情報を更新 final updatedCustomer = _currentInvoice.customer.copyWith( formalName: formalName, ); final updatedInvoice = _currentInvoice.copyWith( customer: updatedCustomer, items: _items, notes: _notesController.text, ); // データベースに保存 await _invoiceRepo.saveInvoice(updatedInvoice); // 顧客の正式名称が変更されている可能性があるため、マスターも更新 if (updatedCustomer.formalName != widget.invoice.customer.formalName) { await _customerRepo.saveCustomer(updatedCustomer); } setState(() => _isEditing = false); final newPath = await generateInvoicePdf(updatedInvoice); if (newPath != null) { final finalInvoice = updatedInvoice.copyWith(filePath: newPath); await _invoiceRepo.saveInvoice(finalInvoice); // パスを更新して再保存 setState(() { _currentInvoice = finalInvoice; _currentFilePath = newPath; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('データベースとPDFを更新しました')), ); } } void _exportCsv() { final csvData = _currentInvoice.toCsv(); Share.share(csvData, subject: '請求書データ_CSV'); } Future _printReceipt() async { final printService = PrintService(); final isConnected = await printService.isConnected; if (!isConnected) { final devices = await printService.getPairedDevices(); if (devices.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ペアリング済みのデバイスが見つかりません。OSの設定を確認してください。"))); return; } final selected = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("プリンターを選択"), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: devices.length, itemBuilder: (context, idx) => ListTile( leading: const Icon(Icons.print), title: Text(devices[idx].name ?? "Unknown Device"), subtitle: Text(devices[idx].macAdress ?? ""), onTap: () => Navigator.pop(context, devices[idx]), ), ), ), ), ); if (selected != null) { final ok = await printService.connect(selected.macAdress!); if (!ok) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("接続に失敗しました。"))); return; } } else { return; } } final success = await printService.printReceipt(_currentInvoice); if (!mounted) return; if (success) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("レシートを印刷しました。"))); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("印刷エラーが発生しました。"))); } } @override Widget build(BuildContext context) { final amountFormatter = NumberFormat("#,###"); return Scaffold( appBar: AppBar( title: const Text("販売アシスト1号 請求書詳細"), backgroundColor: Colors.blueGrey, actions: [ if (!_isEditing) ...[ IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"), if (widget.isUnlocked) 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), const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), _buildItemTable(amountFormatter), if (_isEditing) Padding( padding: const EdgeInsets.only(top: 8.0), child: Wrap( spacing: 12, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _addItem, icon: const Icon(Icons.add), label: const Text("空の行を追加"), ), ElevatedButton.icon( onPressed: _pickFromMaster, icon: const Icon(Icons.list_alt), label: const Text("マスターから選択"), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueGrey.shade700, foregroundColor: Colors.white, ), ), ], ), ), const SizedBox(height: 24), _buildSummarySection(amountFormatter), const SizedBox(height: 24), _buildFooterActions(), ], ), ), ); } Widget _buildHeaderSection() { final dateFormatter = DateFormat('yyyy年MM月dd日'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_isEditing) ...[ TextField( controller: _formalNameController, decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()), ), const SizedBox(height: 12), TextField( controller: _notesController, maxLines: 2, decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()), ), ] else ...[ Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}", style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty) Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)), const SizedBox(height: 4), Text("請求番号: ${_currentInvoice.invoiceNumber}"), Text("発行日: ${dateFormatter.format(_currentInvoice.date)}"), if (_currentInvoice.latitude != null) Padding( padding: const EdgeInsets.only(top: 4.0), child: Row( children: [ const Icon(Icons.location_on, size: 14, color: Colors.blueGrey), const SizedBox(width: 4), Text("座標: ${_currentInvoice.latitude!.toStringAsFixed(4)}, ${_currentInvoice.longitude!.toStringAsFixed(4)}", style: const TextStyle(fontSize: 12, color: Colors.blueGrey)), ], ), ), if (_currentInvoice.notes?.isNotEmpty ?? false) ...[ const SizedBox(height: 8), Text("備考: ${_currentInvoice.notes}", style: const TextStyle(color: Colors.black87)), ] ], ], ); } 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), }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, 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, size: 20, color: Colors.red), onPressed: () => _removeItem(idx)), ]); } else { return TableRow(children: [ _TableCell(item.description), _TableCell(item.quantity.toString()), _TableCell(formatter.format(item.unitPrice)), _TableCell(formatter.format(item.subtotal)), const SizedBox(), ]); } }), ], ); } Widget _buildSummarySection(NumberFormat formatter) { final double currentTaxRate = _isEditing ? _currentInvoice.taxRate : _currentInvoice.taxRate; // 編集時も元の税率を維持 final int subtotal = _isEditing ? _calculateCurrentSubtotal() : _currentInvoice.subtotal; final int tax = (subtotal * currentTaxRate).floor(); final int total = subtotal + tax; return Align( alignment: Alignment.centerRight, child: Container( width: 200, child: Column( children: [ _SummaryRow("小計 (税抜)", formatter.format(subtotal)), if (_companyInfo?.taxDisplayMode == 'normal') _SummaryRow("消費税 (${(currentTaxRate * 100).toInt()}%)", formatter.format(tax)), if (_companyInfo?.taxDisplayMode == 'text_only') _SummaryRow("消費税", "(税別)"), const Divider(), _SummaryRow("合計", "¥${formatter.format(total)}", isBold: true), ], ), ), ); } int _calculateCurrentSubtotal() { return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice)); } Widget _buildFooterActions() { if (_isEditing || _currentFilePath == null) return const SizedBox(); return Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _openPdf, icon: const Icon(Icons.launch), label: const Text("PDFを開く"), style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: _sharePdf, icon: const Icon(Icons.share), label: const Text("共有"), style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: _printReceipt, icon: const Icon(Icons.print), label: const Text("レシート"), style: ElevatedButton.styleFrom(backgroundColor: Colors.blueGrey.shade800, foregroundColor: Colors.white), ), ), ], ); } Future _openPdf() async => await OpenFilex.open(_currentFilePath!); Future _sharePdf() async { if (_currentFilePath != null) { await Share.shareXFiles([XFile(_currentFilePath!)], 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.0), child: TextFormField( initialValue: initialValue, keyboardType: keyboardType, style: const TextStyle(fontSize: 12), decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)), onChanged: onChanged, ), ); } class _SummaryRow extends StatelessWidget { final String label, value; final bool isBold; const _SummaryRow(this.label, this.value, {this.isBold = false}); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)), Text(value, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)), ], ), ); }