見た目修正保守

This commit is contained in:
joe 2026-02-28 04:36:30 +09:00
parent 0c338dc12b
commit b23c400aa8
5 changed files with 433 additions and 121 deletions

View file

@ -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(),
), ),
), ),

View file

@ -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,

View file

@ -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),

View file

@ -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);

View file

@ -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: '会社名・住所・登録番号など',