見た目修正保守
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,
|
||||
scaleEnabled: true,
|
||||
minScale: 0.8,
|
||||
maxScale: 2.0,
|
||||
maxScale: 4.0,
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
department: departmentController.text.isEmpty ? null : departmentController.text,
|
||||
address: addressController.text.isEmpty ? null : addressController.text,
|
||||
tel: telController.text.isEmpty ? null : telController.text,
|
||||
email: emailController.text.isEmpty ? null : emailController.text,
|
||||
headChar1: head1.isEmpty ? _headKana(displayNameController.text) : head1,
|
||||
headChar2: head2.isEmpty ? null : head2,
|
||||
isLocked: customer?.isLocked ?? false,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,36 @@ import 'product_picker_modal.dart';
|
|||
import '../models/company_model.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 {
|
||||
final Invoice invoice;
|
||||
final bool editable;
|
||||
|
|
@ -38,6 +68,9 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
final _companyRepo = CompanyRepository();
|
||||
CompanyInfo? _companyInfo;
|
||||
bool _showFormalWarning = true;
|
||||
final List<_DetailSnapshot> _undoStack = [];
|
||||
final List<_DetailSnapshot> _redoStack = [];
|
||||
bool _isApplyingSnapshot = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -69,12 +102,14 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
setState(() {
|
||||
_items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0));
|
||||
});
|
||||
_pushHistory();
|
||||
}
|
||||
|
||||
void _removeItem(int index) {
|
||||
setState(() {
|
||||
_items.removeAt(index);
|
||||
});
|
||||
_pushHistory();
|
||||
}
|
||||
|
||||
void _pickFromMaster() {
|
||||
|
|
@ -126,6 +161,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
}
|
||||
|
||||
setState(() => _isEditing = false);
|
||||
_undoStack.clear();
|
||||
_redoStack.clear();
|
||||
|
||||
final newPath = await generateInvoicePdf(updatedInvoice);
|
||||
if (newPath != null) {
|
||||
|
|
@ -153,6 +190,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final fmt = NumberFormat("#,###");
|
||||
final isDraft = _currentInvoice.isDraft;
|
||||
final docTypeName = _currentInvoice.documentTypeName;
|
||||
final themeColor = Colors.white; // 常に明色
|
||||
final textColor = Colors.black87;
|
||||
|
||||
|
|
@ -163,26 +201,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(), // 常に表示
|
||||
title: Row(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
title: const Text("A3:伝票詳細"),
|
||||
backgroundColor: Colors.indigo.shade700,
|
||||
actions: [
|
||||
if (locked)
|
||||
|
|
@ -226,6 +245,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
onPressed: (locked || !widget.isUnlocked)
|
||||
? null
|
||||
: () async {
|
||||
_pushHistory(clearRedo: true);
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -243,6 +263,16 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
},
|
||||
),
|
||||
] 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.cancel), onPressed: () => setState(() => _isEditing = false)),
|
||||
]
|
||||
|
|
@ -259,21 +289,33 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
if (isDraft)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(10),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
color: Colors.indigo.shade800,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
border: Border.all(color: Colors.indigo.shade900),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.edit_note, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
children: [
|
||||
const Icon(Icons.edit_note, color: Colors.white70),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"下書き: 未確定・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) ...[
|
||||
TextField(
|
||||
controller: _formalNameController,
|
||||
onChanged: (_) => _pushHistory(),
|
||||
decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
onChanged: (_) => _pushHistory(),
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
|
||||
style: TextStyle(color: textColor),
|
||||
|
|
@ -350,17 +394,6 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
"伝票番号: ${_currentInvoice.invoiceNumber}",
|
||||
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),
|
||||
|
|
@ -369,29 +402,43 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
style: TextStyle(color: textColor.withAlpha((0.8 * 255).round())),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor)),
|
||||
if (_currentInvoice.subject?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text("件名: ${_currentInvoice.subject}",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.indigoAccent)),
|
||||
],
|
||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 16, color: textColor)),
|
||||
if ((_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address) != null)
|
||||
Text("住所: ${_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address}", style: TextStyle(color: textColor)),
|
||||
if ((_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel) != null)
|
||||
Text("TEL: ${_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel}", style: TextStyle(color: textColor)),
|
||||
if ((_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email) != null)
|
||||
Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
|
||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"備考: ${_currentInvoice.notes}",
|
||||
style: TextStyle(color: textColor.withAlpha((0.9 * 255).round())),
|
||||
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}",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor)),
|
||||
if (_currentInvoice.subject?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text("件名: ${_currentInvoice.subject}",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.indigo)),
|
||||
],
|
||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 14, color: textColor)),
|
||||
if ((_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address) != null)
|
||||
Text("住所: ${_currentInvoice.contactAddressSnapshot ?? _currentInvoice.customer.address}", style: TextStyle(color: textColor)),
|
||||
if ((_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel) != null)
|
||||
Text("TEL: ${_currentInvoice.contactTelSnapshot ?? _currentInvoice.customer.tel}", style: TextStyle(color: textColor)),
|
||||
if ((_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email) != null)
|
||||
Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
|
||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"備考: ${_currentInvoice.notes}",
|
||||
style: TextStyle(color: textColor.withAlpha((0.9 * 255).round())),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
|
@ -427,19 +474,28 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
_EditableCell(
|
||||
initialValue: item.description,
|
||||
textColor: textColor,
|
||||
onChanged: (val) => item.description = val,
|
||||
onChanged: (val) {
|
||||
setState(() => item.description = val);
|
||||
_pushHistory();
|
||||
},
|
||||
),
|
||||
_EditableCell(
|
||||
initialValue: item.quantity.toString(),
|
||||
textColor: textColor,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0),
|
||||
onChanged: (val) {
|
||||
setState(() => item.quantity = int.tryParse(val) ?? 0);
|
||||
_pushHistory();
|
||||
},
|
||||
),
|
||||
_EditableCell(
|
||||
initialValue: item.unitPrice.toString(),
|
||||
textColor: textColor,
|
||||
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),
|
||||
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,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade900,
|
||||
color: Colors.indigo,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -525,6 +581,61 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
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) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,18 @@ class InvoiceInputForm extends StatefulWidget {
|
|||
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> {
|
||||
final _repository = InvoiceRepository();
|
||||
final InvoiceRepository _invoiceRepo = InvoiceRepository();
|
||||
|
|
@ -40,24 +52,34 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
bool _isDraft = true; // デフォルトは下書き
|
||||
final TextEditingController _subjectController = TextEditingController(); // 追加
|
||||
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 = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_subjectController.addListener(_onSubjectChanged);
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subjectController.removeListener(_onSubjectChanged);
|
||||
_subjectController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialData() async {
|
||||
_repository.cleanupOrphanedPdfs();
|
||||
final customerRepo = CustomerRepository();
|
||||
final customers = await customerRepo.getAllCustomers();
|
||||
if (customers.isNotEmpty) {
|
||||
setState(() => _selectedCustomer = customers.first);
|
||||
}
|
||||
|
||||
await customerRepo.getAllCustomers();
|
||||
|
||||
setState(() {
|
||||
// 既存伝票がある場合は初期値を上書き
|
||||
if (widget.existingInvoice != null) {
|
||||
|
|
@ -77,6 +99,12 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
_documentType = widget.initialDocumentType;
|
||||
}
|
||||
});
|
||||
_pushHistory(clearRedo: true);
|
||||
}
|
||||
|
||||
void _onSubjectChanged() {
|
||||
if (_isApplyingSnapshot) return;
|
||||
_pushHistory();
|
||||
}
|
||||
|
||||
void _addItem() {
|
||||
|
|
@ -93,6 +121,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
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
|
||||
Widget build(BuildContext context) {
|
||||
final fmt = NumberFormat("#,###");
|
||||
|
|
@ -219,6 +326,18 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
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(
|
||||
children: [
|
||||
|
|
@ -281,6 +400,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
);
|
||||
if (picked != null) {
|
||||
setState(() => _selectedDate = picked);
|
||||
_pushHistory();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
|
@ -324,6 +444,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
);
|
||||
if (picked != null) {
|
||||
setState(() => _selectedCustomer = picked);
|
||||
_pushHistory();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
@ -357,6 +478,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
final item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
_pushHistory();
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, idx) {
|
||||
|
|
@ -376,56 +498,72 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
|
||||
onPressed: () => setState(() => _items.removeAt(idx)),
|
||||
onPressed: () {
|
||||
setState(() => _items.removeAt(idx));
|
||||
_pushHistory();
|
||||
},
|
||||
tooltip: "削除",
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// 簡易編集ダイアログ
|
||||
// 簡易編集ダイアログ(キーボードでせり上げない)
|
||||
final descCtrl = TextEditingController(text: item.description);
|
||||
final qtyCtrl = TextEditingController(text: item.quantity.toString());
|
||||
final priceCtrl = TextEditingController(text: item.unitPrice.toString());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("明細の編集"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
||||
TextField(controller: qtyCtrl, decoration: const InputDecoration(labelText: "数量"), keyboardType: TextInputType.number),
|
||||
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.search, size: 18),
|
||||
label: const Text("マスター参照"),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context); // close edit dialog before jumping
|
||||
await Navigator.push(
|
||||
this.context,
|
||||
MaterialPageRoute(builder: (_) => const ProductMasterScreen()),
|
||||
);
|
||||
},
|
||||
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("明細の編集"),
|
||||
content: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: inset + 12),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: descCtrl, decoration: const InputDecoration(labelText: "品名 / 項目")),
|
||||
TextField(controller: qtyCtrl, decoration: const InputDecoration(labelText: "数量"), keyboardType: TextInputType.number),
|
||||
TextField(controller: priceCtrl, decoration: const InputDecoration(labelText: "単価"), keyboardType: TextInputType.number),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.search, size: 18),
|
||||
label: const Text("マスター参照"),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context); // close edit dialog before jumping
|
||||
await Navigator.push(
|
||||
this.context,
|
||||
MaterialPageRoute(builder: (_) => const ProductMasterScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_items[idx] = item.copyWith(
|
||||
description: descCtrl.text,
|
||||
quantity: int.tryParse(qtyCtrl.text) ?? item.quantity,
|
||||
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
||||
);
|
||||
});
|
||||
_pushHistory();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("更新"),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_items[idx] = item.copyWith(
|
||||
description: descCtrl.text,
|
||||
quantity: int.tryParse(qtyCtrl.text) ?? item.quantity,
|
||||
unitPrice: int.tryParse(priceCtrl.text) ?? item.unitPrice,
|
||||
);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text("更新"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -446,7 +584,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade900,
|
||||
color: Colors.indigo,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -584,16 +722,23 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
children: [
|
||||
Text("案件名 / 件名", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _subjectController,
|
||||
style: TextStyle(color: textColor),
|
||||
decoration: InputDecoration(
|
||||
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
||||
hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
|
||||
filled: true,
|
||||
fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _subjectController,
|
||||
style: TextStyle(color: textColor),
|
||||
decoration: InputDecoration(
|
||||
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
||||
hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -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 {
|
||||
final List<Offset?> points;
|
||||
SignaturePainter(this.points);
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('S1:設定'),
|
||||
backgroundColor: Colors.indigo,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
|
|
@ -246,6 +247,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.only(bottom: listBottomPadding),
|
||||
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(
|
||||
title: '自社情報',
|
||||
subtitle: '会社名・住所・登録番号など',
|
||||
|
|
|
|||
Loading…
Reference in a new issue