249 lines
No EOL
9 KiB
Dart
249 lines
No EOL
9 KiB
Dart
// 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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |