見た目修正保守
This commit is contained in:
parent
0c338dc12b
commit
b23c400aa8
5 changed files with 433 additions and 121 deletions
|
|
@ -73,7 +73,7 @@ class MyApp extends StatelessWidget {
|
||||||
panEnabled: false,
|
panEnabled: false,
|
||||||
scaleEnabled: true,
|
scaleEnabled: true,
|
||||||
minScale: 0.8,
|
minScale: 0.8,
|
||||||
maxScale: 2.0,
|
maxScale: 4.0,
|
||||||
child: child ?? const SizedBox.shrink(),
|
child: child ?? const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
department: departmentController.text.isEmpty ? null : departmentController.text,
|
department: departmentController.text.isEmpty ? null : departmentController.text,
|
||||||
address: addressController.text.isEmpty ? null : addressController.text,
|
address: addressController.text.isEmpty ? null : addressController.text,
|
||||||
tel: telController.text.isEmpty ? null : telController.text,
|
tel: telController.text.isEmpty ? null : telController.text,
|
||||||
|
email: emailController.text.isEmpty ? null : emailController.text,
|
||||||
headChar1: head1.isEmpty ? _headKana(displayNameController.text) : head1,
|
headChar1: head1.isEmpty ? _headKana(displayNameController.text) : head1,
|
||||||
headChar2: head2.isEmpty ? null : head2,
|
headChar2: head2.isEmpty ? null : head2,
|
||||||
isLocked: customer?.isLocked ?? false,
|
isLocked: customer?.isLocked ?? false,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,36 @@ import 'product_picker_modal.dart';
|
||||||
import '../models/company_model.dart';
|
import '../models/company_model.dart';
|
||||||
import '../widgets/keyboard_inset_wrapper.dart';
|
import '../widgets/keyboard_inset_wrapper.dart';
|
||||||
|
|
||||||
|
class _DetailSnapshot {
|
||||||
|
final String formalName;
|
||||||
|
final String notes;
|
||||||
|
final List<InvoiceItem> items;
|
||||||
|
final double taxRate;
|
||||||
|
final bool includeTax;
|
||||||
|
final bool isDraft;
|
||||||
|
|
||||||
|
const _DetailSnapshot({
|
||||||
|
required this.formalName,
|
||||||
|
required this.notes,
|
||||||
|
required this.items,
|
||||||
|
required this.taxRate,
|
||||||
|
required this.includeTax,
|
||||||
|
required this.isDraft,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<InvoiceItem> _cloneItemsDetail(List<InvoiceItem> source) {
|
||||||
|
return source
|
||||||
|
.map((e) => InvoiceItem(
|
||||||
|
id: e.id,
|
||||||
|
productId: e.productId,
|
||||||
|
description: e.description,
|
||||||
|
quantity: e.quantity,
|
||||||
|
unitPrice: e.unitPrice,
|
||||||
|
))
|
||||||
|
.toList(growable: true);
|
||||||
|
}
|
||||||
|
|
||||||
class InvoiceDetailPage extends StatefulWidget {
|
class InvoiceDetailPage extends StatefulWidget {
|
||||||
final Invoice invoice;
|
final Invoice invoice;
|
||||||
final bool editable;
|
final bool editable;
|
||||||
|
|
@ -38,6 +68,9 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
final _companyRepo = CompanyRepository();
|
final _companyRepo = CompanyRepository();
|
||||||
CompanyInfo? _companyInfo;
|
CompanyInfo? _companyInfo;
|
||||||
bool _showFormalWarning = true;
|
bool _showFormalWarning = true;
|
||||||
|
final List<_DetailSnapshot> _undoStack = [];
|
||||||
|
final List<_DetailSnapshot> _redoStack = [];
|
||||||
|
bool _isApplyingSnapshot = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -69,12 +102,14 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0));
|
_items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0));
|
||||||
});
|
});
|
||||||
|
_pushHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeItem(int index) {
|
void _removeItem(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_items.removeAt(index);
|
_items.removeAt(index);
|
||||||
});
|
});
|
||||||
|
_pushHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pickFromMaster() {
|
void _pickFromMaster() {
|
||||||
|
|
@ -126,6 +161,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isEditing = false);
|
setState(() => _isEditing = false);
|
||||||
|
_undoStack.clear();
|
||||||
|
_redoStack.clear();
|
||||||
|
|
||||||
final newPath = await generateInvoicePdf(updatedInvoice);
|
final newPath = await generateInvoicePdf(updatedInvoice);
|
||||||
if (newPath != null) {
|
if (newPath != null) {
|
||||||
|
|
@ -153,6 +190,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fmt = NumberFormat("#,###");
|
final fmt = NumberFormat("#,###");
|
||||||
final isDraft = _currentInvoice.isDraft;
|
final isDraft = _currentInvoice.isDraft;
|
||||||
|
final docTypeName = _currentInvoice.documentTypeName;
|
||||||
final themeColor = Colors.white; // 常に明色
|
final themeColor = Colors.white; // 常に明色
|
||||||
final textColor = Colors.black87;
|
final textColor = Colors.black87;
|
||||||
|
|
||||||
|
|
@ -163,26 +201,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const BackButton(), // 常に表示
|
leading: const BackButton(), // 常に表示
|
||||||
title: Row(
|
title: const Text("A3:伝票詳細"),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
isDraft ? "A3:伝票詳細" : "A3:伝票詳細",
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
if (isDraft)
|
|
||||||
Chip(
|
|
||||||
label: const Text("下書き", style: TextStyle(color: Colors.white)),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.indigo.shade700,
|
backgroundColor: Colors.indigo.shade700,
|
||||||
actions: [
|
actions: [
|
||||||
if (locked)
|
if (locked)
|
||||||
|
|
@ -226,6 +245,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
onPressed: (locked || !widget.isUnlocked)
|
onPressed: (locked || !widget.isUnlocked)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
_pushHistory(clearRedo: true);
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
@ -243,6 +263,16 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.undo),
|
||||||
|
onPressed: _undoStack.isNotEmpty ? _undo : null,
|
||||||
|
tooltip: "元に戻す",
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.redo),
|
||||||
|
onPressed: _redoStack.isNotEmpty ? _redo : null,
|
||||||
|
tooltip: "やり直す",
|
||||||
|
),
|
||||||
IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges),
|
IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges),
|
||||||
IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)),
|
IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)),
|
||||||
]
|
]
|
||||||
|
|
@ -259,21 +289,33 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
if (isDraft)
|
if (isDraft)
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(10),
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange.shade50,
|
color: Colors.indigo.shade800,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.orange.shade200),
|
border: Border.all(color: Colors.indigo.shade900),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.edit_note, color: Colors.orange),
|
const Icon(Icons.edit_note, color: Colors.white70),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
"下書き: 未確定・PDFは正式発行で確定",
|
"下書き: 未確定・PDFは正式発行で確定",
|
||||||
style: TextStyle(color: Colors.orange),
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade600,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"下書${docTypeName}",
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -332,12 +374,14 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
if (_isEditing) ...[
|
if (_isEditing) ...[
|
||||||
TextField(
|
TextField(
|
||||||
controller: _formalNameController,
|
controller: _formalNameController,
|
||||||
|
onChanged: (_) => _pushHistory(),
|
||||||
decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
|
decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
|
||||||
style: TextStyle(color: textColor),
|
style: TextStyle(color: textColor),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _notesController,
|
controller: _notesController,
|
||||||
|
onChanged: (_) => _pushHistory(),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
|
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
|
||||||
style: TextStyle(color: textColor),
|
style: TextStyle(color: textColor),
|
||||||
|
|
@ -350,17 +394,6 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
"伝票番号: ${_currentInvoice.invoiceNumber}",
|
"伝票番号: ${_currentInvoice.invoiceNumber}",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor),
|
||||||
),
|
),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _currentInvoice.isDraft ? Colors.orange : Colors.green.shade700,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_currentInvoice.isDraft ? "下書き" : "確定済",
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -369,16 +402,27 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
style: TextStyle(color: textColor.withAlpha((0.8 * 255).round())),
|
style: TextStyle(color: textColor.withAlpha((0.8 * 255).round())),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("取引先", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor)),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
if (_currentInvoice.subject?.isNotEmpty ?? false) ...[
|
if (_currentInvoice.subject?.isNotEmpty ?? false) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
Text("件名: ${_currentInvoice.subject}",
|
Text("件名: ${_currentInvoice.subject}",
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.indigoAccent)),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.indigo)),
|
||||||
],
|
],
|
||||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||||
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 16, color: textColor)),
|
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 14, color: textColor)),
|
||||||
if ((_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address) != null)
|
if ((_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address) != null)
|
||||||
Text("住所: ${_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address}", style: TextStyle(color: textColor)),
|
Text("住所: ${_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address}", style: TextStyle(color: textColor)),
|
||||||
if ((_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel) != null)
|
if ((_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel) != null)
|
||||||
|
|
@ -386,13 +430,16 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
if ((_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email) != null)
|
if ((_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email) != null)
|
||||||
Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
|
Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
|
||||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
"備考: ${_currentInvoice.notes}",
|
"備考: ${_currentInvoice.notes}",
|
||||||
style: TextStyle(color: textColor.withAlpha((0.9 * 255).round())),
|
style: TextStyle(color: textColor.withAlpha((0.9 * 255).round())),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -427,19 +474,28 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
_EditableCell(
|
_EditableCell(
|
||||||
initialValue: item.description,
|
initialValue: item.description,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
onChanged: (val) => item.description = val,
|
onChanged: (val) {
|
||||||
|
setState(() => item.description = val);
|
||||||
|
_pushHistory();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_EditableCell(
|
_EditableCell(
|
||||||
initialValue: item.quantity.toString(),
|
initialValue: item.quantity.toString(),
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0),
|
onChanged: (val) {
|
||||||
|
setState(() => item.quantity = int.tryParse(val) ?? 0);
|
||||||
|
_pushHistory();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_EditableCell(
|
_EditableCell(
|
||||||
initialValue: item.unitPrice.toString(),
|
initialValue: item.unitPrice.toString(),
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0),
|
onChanged: (val) {
|
||||||
|
setState(() => item.unitPrice = int.tryParse(val) ?? 0);
|
||||||
|
_pushHistory();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_TableCell(formatter.format(item.subtotal), textColor: textColor),
|
_TableCell(formatter.format(item.subtotal), textColor: textColor),
|
||||||
IconButton(icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () => _removeItem(idx)),
|
IconButton(icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () => _removeItem(idx)),
|
||||||
|
|
@ -468,7 +524,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.indigo.shade900,
|
color: Colors.indigo,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -525,6 +581,61 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice));
|
return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _pushHistory({bool clearRedo = false}) {
|
||||||
|
if (!_isEditing || _isApplyingSnapshot) return;
|
||||||
|
if (_undoStack.length >= 30) _undoStack.removeAt(0);
|
||||||
|
_undoStack.add(_DetailSnapshot(
|
||||||
|
formalName: _formalNameController.text,
|
||||||
|
notes: _notesController.text,
|
||||||
|
items: _cloneItemsDetail(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
isDraft: _currentInvoice.isDraft,
|
||||||
|
));
|
||||||
|
if (clearRedo) _redoStack.clear();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _undo() {
|
||||||
|
if (_undoStack.isEmpty) return;
|
||||||
|
final snapshot = _undoStack.removeLast();
|
||||||
|
_redoStack.add(_DetailSnapshot(
|
||||||
|
formalName: _formalNameController.text,
|
||||||
|
notes: _notesController.text,
|
||||||
|
items: _cloneItemsDetail(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
isDraft: _currentInvoice.isDraft,
|
||||||
|
));
|
||||||
|
_applySnapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _redo() {
|
||||||
|
if (_redoStack.isEmpty) return;
|
||||||
|
final snapshot = _redoStack.removeLast();
|
||||||
|
_undoStack.add(_DetailSnapshot(
|
||||||
|
formalName: _formalNameController.text,
|
||||||
|
notes: _notesController.text,
|
||||||
|
items: _cloneItemsDetail(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
isDraft: _currentInvoice.isDraft,
|
||||||
|
));
|
||||||
|
_applySnapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applySnapshot(_DetailSnapshot snapshot) {
|
||||||
|
_isApplyingSnapshot = true;
|
||||||
|
setState(() {
|
||||||
|
_formalNameController.text = snapshot.formalName;
|
||||||
|
_notesController.text = snapshot.notes;
|
||||||
|
_items = _cloneItemsDetail(snapshot.items);
|
||||||
|
_taxRate = snapshot.taxRate;
|
||||||
|
_includeTax = snapshot.includeTax;
|
||||||
|
});
|
||||||
|
_isApplyingSnapshot = false;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildExperimentalSection(bool isDraft) {
|
Widget _buildExperimentalSection(bool isDraft) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,18 @@ class InvoiceInputForm extends StatefulWidget {
|
||||||
State<InvoiceInputForm> createState() => _InvoiceInputFormState();
|
State<InvoiceInputForm> createState() => _InvoiceInputFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<InvoiceItem> _cloneItems(List<InvoiceItem> source) {
|
||||||
|
return source
|
||||||
|
.map((e) => InvoiceItem(
|
||||||
|
id: e.id,
|
||||||
|
productId: e.productId,
|
||||||
|
description: e.description,
|
||||||
|
quantity: e.quantity,
|
||||||
|
unitPrice: e.unitPrice,
|
||||||
|
))
|
||||||
|
.toList(growable: true);
|
||||||
|
}
|
||||||
|
|
||||||
class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
final _repository = InvoiceRepository();
|
final _repository = InvoiceRepository();
|
||||||
final InvoiceRepository _invoiceRepo = InvoiceRepository();
|
final InvoiceRepository _invoiceRepo = InvoiceRepository();
|
||||||
|
|
@ -40,6 +52,11 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
bool _isDraft = true; // デフォルトは下書き
|
bool _isDraft = true; // デフォルトは下書き
|
||||||
final TextEditingController _subjectController = TextEditingController(); // 追加
|
final TextEditingController _subjectController = TextEditingController(); // 追加
|
||||||
bool _isSaving = false; // 保存中フラグ
|
bool _isSaving = false; // 保存中フラグ
|
||||||
|
final List<_InvoiceSnapshot> _undoStack = [];
|
||||||
|
final List<_InvoiceSnapshot> _redoStack = [];
|
||||||
|
bool _isApplyingSnapshot = false;
|
||||||
|
bool get _canUndo => _undoStack.length > 1;
|
||||||
|
bool get _canRedo => _redoStack.isNotEmpty;
|
||||||
|
|
||||||
// 署名用の実験的パス
|
// 署名用の実験的パス
|
||||||
final List<Offset?> _signaturePath = [];
|
final List<Offset?> _signaturePath = [];
|
||||||
|
|
@ -47,16 +64,21 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_subjectController.addListener(_onSubjectChanged);
|
||||||
_loadInitialData();
|
_loadInitialData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subjectController.removeListener(_onSubjectChanged);
|
||||||
|
_subjectController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadInitialData() async {
|
Future<void> _loadInitialData() async {
|
||||||
_repository.cleanupOrphanedPdfs();
|
_repository.cleanupOrphanedPdfs();
|
||||||
final customerRepo = CustomerRepository();
|
final customerRepo = CustomerRepository();
|
||||||
final customers = await customerRepo.getAllCustomers();
|
await customerRepo.getAllCustomers();
|
||||||
if (customers.isNotEmpty) {
|
|
||||||
setState(() => _selectedCustomer = customers.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// 既存伝票がある場合は初期値を上書き
|
// 既存伝票がある場合は初期値を上書き
|
||||||
|
|
@ -77,6 +99,12 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
_documentType = widget.initialDocumentType;
|
_documentType = widget.initialDocumentType;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_pushHistory(clearRedo: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSubjectChanged() {
|
||||||
|
if (_isApplyingSnapshot) return;
|
||||||
|
_pushHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addItem() {
|
void _addItem() {
|
||||||
|
|
@ -93,6 +121,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
unitPrice: product.defaultUnitPrice,
|
unitPrice: product.defaultUnitPrice,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
_pushHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,6 +236,84 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _pushHistory({bool clearRedo = false}) {
|
||||||
|
setState(() {
|
||||||
|
if (_undoStack.length >= 30) _undoStack.removeAt(0);
|
||||||
|
_undoStack.add(_InvoiceSnapshot(
|
||||||
|
customer: _selectedCustomer,
|
||||||
|
items: _cloneItems(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
documentType: _documentType,
|
||||||
|
date: _selectedDate,
|
||||||
|
isDraft: _isDraft,
|
||||||
|
subject: _subjectController.text,
|
||||||
|
));
|
||||||
|
if (clearRedo) _redoStack.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _undo() {
|
||||||
|
if (_undoStack.length <= 1) return; // 直前状態がない
|
||||||
|
setState(() {
|
||||||
|
// 現在の状態をredoへ積む
|
||||||
|
_redoStack.add(_InvoiceSnapshot(
|
||||||
|
customer: _selectedCustomer,
|
||||||
|
items: _cloneItems(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
documentType: _documentType,
|
||||||
|
date: _selectedDate,
|
||||||
|
isDraft: _isDraft,
|
||||||
|
subject: _subjectController.text,
|
||||||
|
));
|
||||||
|
// 一番新しい履歴を捨て、直前のスナップショットを適用
|
||||||
|
_undoStack.removeLast();
|
||||||
|
final snapshot = _undoStack.last;
|
||||||
|
_isApplyingSnapshot = true;
|
||||||
|
_selectedCustomer = snapshot.customer;
|
||||||
|
_items
|
||||||
|
..clear()
|
||||||
|
..addAll(_cloneItems(snapshot.items));
|
||||||
|
_taxRate = snapshot.taxRate;
|
||||||
|
_includeTax = snapshot.includeTax;
|
||||||
|
_documentType = snapshot.documentType;
|
||||||
|
_selectedDate = snapshot.date;
|
||||||
|
_isDraft = snapshot.isDraft;
|
||||||
|
_subjectController.text = snapshot.subject;
|
||||||
|
_isApplyingSnapshot = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _redo() {
|
||||||
|
if (_redoStack.isEmpty) return;
|
||||||
|
setState(() {
|
||||||
|
_undoStack.add(_InvoiceSnapshot(
|
||||||
|
customer: _selectedCustomer,
|
||||||
|
items: _cloneItems(_items),
|
||||||
|
taxRate: _taxRate,
|
||||||
|
includeTax: _includeTax,
|
||||||
|
documentType: _documentType,
|
||||||
|
date: _selectedDate,
|
||||||
|
isDraft: _isDraft,
|
||||||
|
subject: _subjectController.text,
|
||||||
|
));
|
||||||
|
final snapshot = _redoStack.removeLast();
|
||||||
|
_isApplyingSnapshot = true;
|
||||||
|
_selectedCustomer = snapshot.customer;
|
||||||
|
_items
|
||||||
|
..clear()
|
||||||
|
..addAll(_cloneItems(snapshot.items));
|
||||||
|
_taxRate = snapshot.taxRate;
|
||||||
|
_includeTax = snapshot.includeTax;
|
||||||
|
_documentType = snapshot.documentType;
|
||||||
|
_selectedDate = snapshot.date;
|
||||||
|
_isDraft = snapshot.isDraft;
|
||||||
|
_subjectController.text = snapshot.subject;
|
||||||
|
_isApplyingSnapshot = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fmt = NumberFormat("#,###");
|
final fmt = NumberFormat("#,###");
|
||||||
|
|
@ -219,6 +326,18 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: const Text("A1:伝票入力"),
|
title: const Text("A1:伝票入力"),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.undo),
|
||||||
|
onPressed: _canUndo ? _undo : null,
|
||||||
|
tooltip: "元に戻す",
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.redo),
|
||||||
|
onPressed: _canRedo ? _redo : null,
|
||||||
|
tooltip: "やり直す",
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -281,6 +400,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() => _selectedDate = picked);
|
setState(() => _selectedDate = picked);
|
||||||
|
_pushHistory();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -324,6 +444,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() => _selectedCustomer = picked);
|
setState(() => _selectedCustomer = picked);
|
||||||
|
_pushHistory();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -357,6 +478,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
final item = _items.removeAt(oldIndex);
|
final item = _items.removeAt(oldIndex);
|
||||||
_items.insert(newIndex, item);
|
_items.insert(newIndex, item);
|
||||||
});
|
});
|
||||||
|
_pushHistory();
|
||||||
},
|
},
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
|
|
@ -376,21 +498,33 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
|
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
|
||||||
onPressed: () => setState(() => _items.removeAt(idx)),
|
onPressed: () {
|
||||||
|
setState(() => _items.removeAt(idx));
|
||||||
|
_pushHistory();
|
||||||
|
},
|
||||||
tooltip: "削除",
|
tooltip: "削除",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// 簡易編集ダイアログ
|
// 簡易編集ダイアログ(キーボードでせり上げない)
|
||||||
final descCtrl = TextEditingController(text: item.description);
|
final descCtrl = TextEditingController(text: item.description);
|
||||||
final qtyCtrl = TextEditingController(text: item.quantity.toString());
|
final qtyCtrl = TextEditingController(text: item.quantity.toString());
|
||||||
final priceCtrl = TextEditingController(text: item.unitPrice.toString());
|
final priceCtrl = TextEditingController(text: item.unitPrice.toString());
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) {
|
||||||
|
final inset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
return MediaQuery.removeViewInsets(
|
||||||
|
removeBottom: true,
|
||||||
|
context: context,
|
||||||
|
child: AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
title: const Text("明細の編集"),
|
title: const Text("明細の編集"),
|
||||||
content: Column(
|
content: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(bottom: inset + 12),
|
||||||
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
||||||
|
|
@ -398,6 +532,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.search, size: 18),
|
icon: const Icon(Icons.search, size: 18),
|
||||||
|
|
@ -420,6 +555,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
_pushHistory();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text("更新"),
|
child: const Text("更新"),
|
||||||
|
|
@ -428,6 +564,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -446,7 +584,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.indigo.shade900,
|
color: Colors.indigo,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -584,16 +722,23 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
children: [
|
children: [
|
||||||
Text("案件名 / 件名", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
Text("案件名 / 件名", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
controller: _subjectController,
|
controller: _subjectController,
|
||||||
style: TextStyle(color: textColor),
|
style: TextStyle(color: textColor),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
||||||
hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
|
hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
|
||||||
filled: true,
|
border: InputBorder.none,
|
||||||
fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100,
|
isDense: true,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -601,6 +746,28 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _InvoiceSnapshot {
|
||||||
|
final Customer? customer;
|
||||||
|
final List<InvoiceItem> items;
|
||||||
|
final double taxRate;
|
||||||
|
final bool includeTax;
|
||||||
|
final DocumentType documentType;
|
||||||
|
final DateTime date;
|
||||||
|
final bool isDraft;
|
||||||
|
final String subject;
|
||||||
|
|
||||||
|
_InvoiceSnapshot({
|
||||||
|
required this.customer,
|
||||||
|
required this.items,
|
||||||
|
required this.taxRate,
|
||||||
|
required this.includeTax,
|
||||||
|
required this.documentType,
|
||||||
|
required this.date,
|
||||||
|
required this.isDraft,
|
||||||
|
required this.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class SignaturePainter extends CustomPainter {
|
class SignaturePainter extends CustomPainter {
|
||||||
final List<Offset?> points;
|
final List<Offset?> points;
|
||||||
SignaturePainter(this.points);
|
SignaturePainter(this.points);
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('S1:設定'),
|
title: const Text('S1:設定'),
|
||||||
|
backgroundColor: Colors.indigo,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.info_outline),
|
icon: const Icon(Icons.info_outline),
|
||||||
|
|
@ -246,6 +247,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: EdgeInsets.only(bottom: listBottomPadding),
|
padding: EdgeInsets.only(bottom: listBottomPadding),
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.indigo.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.indigo.shade100),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.business, color: Colors.indigo, size: 28),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
"自社情報を開く",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.indigo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CompanyInfoScreen())),
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
label: const Text("詳細"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.indigo,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
_section(
|
_section(
|
||||||
title: '自社情報',
|
title: '自社情報',
|
||||||
subtitle: '会社名・住所・登録番号など',
|
subtitle: '会社名・住所・登録番号など',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue