diff --git a/lib/main.dart b/lib/main.dart index 495810d..e98aaff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ), ), diff --git a/lib/screens/customer_master_screen.dart b/lib/screens/customer_master_screen.dart index bdea89f..76a01da 100644 --- a/lib/screens/customer_master_screen.dart +++ b/lib/screens/customer_master_screen.dart @@ -416,6 +416,7 @@ class _CustomerMasterScreenState extends State { 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, diff --git a/lib/screens/invoice_detail_page.dart b/lib/screens/invoice_detail_page.dart index 9574e48..a608d59 100644 --- a/lib/screens/invoice_detail_page.dart +++ b/lib/screens/invoice_detail_page.dart @@ -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 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 _cloneItemsDetail(List 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 { 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 { 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 { } setState(() => _isEditing = false); + _undoStack.clear(); + _redoStack.clear(); final newPath = await generateInvoicePdf(updatedInvoice); if (newPath != null) { @@ -153,6 +190,7 @@ class _InvoiceDetailPageState extends State { 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 { 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 { onPressed: (locked || !widget.isUnlocked) ? null : () async { + _pushHistory(clearRedo: true); await Navigator.push( context, MaterialPageRoute( @@ -243,6 +263,16 @@ class _InvoiceDetailPageState extends State { }, ), ] 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 { 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 { 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 { "伝票番号: ${_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 { 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 { _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 { 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 { 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), diff --git a/lib/screens/invoice_input_screen.dart b/lib/screens/invoice_input_screen.dart index 5809ea0..bd018e6 100644 --- a/lib/screens/invoice_input_screen.dart +++ b/lib/screens/invoice_input_screen.dart @@ -28,6 +28,18 @@ class InvoiceInputForm extends StatefulWidget { State createState() => _InvoiceInputFormState(); } +List _cloneItems(List 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 { final _repository = InvoiceRepository(); final InvoiceRepository _invoiceRepo = InvoiceRepository(); @@ -40,24 +52,34 @@ class _InvoiceInputFormState extends State { 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 _signaturePath = []; @override void initState() { super.initState(); + _subjectController.addListener(_onSubjectChanged); _loadInitialData(); } + @override + void dispose() { + _subjectController.removeListener(_onSubjectChanged); + _subjectController.dispose(); + super.dispose(); + } + Future _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 { _documentType = widget.initialDocumentType; } }); + _pushHistory(clearRedo: true); + } + + void _onSubjectChanged() { + if (_isApplyingSnapshot) return; + _pushHistory(); } void _addItem() { @@ -93,6 +121,7 @@ class _InvoiceInputFormState extends State { unitPrice: product.defaultUnitPrice, )); }); + _pushHistory(); }); } @@ -207,6 +236,84 @@ class _InvoiceInputFormState extends State { ); } + 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 { 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 { ); if (picked != null) { setState(() => _selectedDate = picked); + _pushHistory(); } }, child: Container( @@ -324,6 +444,7 @@ class _InvoiceInputFormState extends State { ); if (picked != null) { setState(() => _selectedCustomer = picked); + _pushHistory(); } }, ), @@ -357,6 +478,7 @@ class _InvoiceInputFormState extends State { final item = _items.removeAt(oldIndex); _items.insert(newIndex, item); }); + _pushHistory(); }, buildDefaultDragHandles: false, itemBuilder: (context, idx) { @@ -376,56 +498,72 @@ class _InvoiceInputFormState extends State { 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 { 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 { 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 { } } +class _InvoiceSnapshot { + final Customer? customer; + final List 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 points; SignaturePainter(this.points); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 2f42080..652fbe1 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -232,6 +232,7 @@ class _SettingsScreenState extends State { 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 { 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: '会社名・住所・登録番号など',