h-1.flet.3/flutter.参考/lib/screens/invoice_detail_page.dart
2026-02-20 23:24:01 +09:00

249 lines
No EOL
9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/screens/invoice_detail_page.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.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 Invoice _currentInvoice;
final _descriptionController = TextEditingController();
final _quantityController = TextEditingController();
final _unitPriceController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_currentInvoice = widget.invoice;
}
@override
void dispose() {
_descriptionController.dispose();
_quantityController.dispose();
_unitPriceController.dispose();
super.dispose();
}
/// 明細を追加
void _addItem() {
setState(() {
_currentInvoice = _currentInvoice.copyWith(
items: [
..._currentInvoice.items,
InvoiceItem(
description: "新規明細",
quantity: 1,
unitPrice: 10000,
)
]
);
});
}
/// 明細を削除
void _removeItem(int index) {
setState(() {
final newItems = List<InvoiceItem>.from(_currentInvoice.items);
newItems.removeAt(index);
_currentInvoice = _currentInvoice.copyWith(items: newItems);
});
}
/// 明細を更新
void _updateItem(int index, InvoiceItem item) {
setState(() {
final newItems = List<InvoiceItem>.from(_currentInvoice.items);
newItems[index] = item;
_currentInvoice = _currentInvoice.copyWith(items: newItems);
});
}
/// PDFを再生成
Future<void> _regeneratePdf() async {
setState(() => _isLoading = true);
final path = await generateInvoicePdf(_currentInvoice);
if (path != null) {
final updatedInvoice = _currentInvoice.copyWith(filePath: path);
// TODO: Repositoryで保存
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFを更新しました')),
);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFの生成に失敗しました')),
);
}
}
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
final dateFormatter = DateFormat('yyyy/MM/dd');
return Scaffold(
appBar: AppBar(
title: Text("${_currentInvoice.type.label}詳細"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
// TODO: 共有機能
},
),
IconButton(
icon: const Icon(Icons.print),
onPressed: _regeneratePdf,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本情報
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"基本情報",
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text("種類: ${_currentInvoice.type.label}"),
Text("顧客: ${_currentInvoice.customer.formalName}"),
Text("日付: ${dateFormatter.format(_currentInvoice.date)}"),
Text("番号: ${_currentInvoice.invoiceNumber}"),
if (_currentInvoice.notes != null)
Text("備考: ${_currentInvoice.notes}"),
],
),
),
),
const SizedBox(height: 16),
// 明細リスト
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"明細",
style: Theme.of(context).textTheme.titleLarge,
),
TextButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text("明細追加"),
),
],
),
const SizedBox(height: 8),
..._currentInvoice.items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.description,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
"${item.quantity} × ¥${amountFormatter.format(item.unitPrice)}",
style: TextStyle(
color: item.isDiscount ? Colors.red : null,
decoration: item.isDiscount
? TextDecoration.lineThrough
: null,
),
),
],
),
),
Text(
"¥${amountFormatter.format(item.subtotal)}",
style: TextStyle(
fontWeight: FontWeight.bold,
color: item.isDiscount ? Colors.red : null,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeItem(index),
),
],
),
);
}).toList(),
],
),
),
),
const SizedBox(height: 16),
// 合計
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("小計: ¥${amountFormatter.format(_currentInvoice.subtotal)}"),
Text("消費税: ¥${amountFormatter.format(_currentInvoice.tax)}"),
Text(
"合計: ¥${amountFormatter.format(_currentInvoice.totalAmount)}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
);
}
}