hanbai1/lib/screens/invoice_detail_page.dart
2026-02-09 09:06:36 +09:00

240 lines
No EOL
9.9 KiB
Dart

// 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<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
}
class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
late TextEditingController _formalNameController;
late TextEditingController _notesController;
late List<InvoiceItem> _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<void> _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<db.InvoiceItemsCompanion> 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<int>(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));
}