From bab533fccb8c873f6d2fd63d3a7497f63fae43f4 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 25 Feb 2026 19:53:33 +0900 Subject: [PATCH] chore: flesh out settings template and lock badges --- lib/screens/invoice_detail_page.dart | 31 +-- lib/screens/invoice_history_screen.dart | 25 ++- lib/screens/settings_screen.dart | 277 +++++++++++++++++------- 3 files changed, 237 insertions(+), 96 deletions(-) diff --git a/lib/screens/invoice_detail_page.dart b/lib/screens/invoice_detail_page.dart index 1eed9ed..5cb80e0 100644 --- a/lib/screens/invoice_detail_page.dart +++ b/lib/screens/invoice_detail_page.dart @@ -150,6 +150,8 @@ class _InvoiceDetailPageState extends State { final themeColor = isDraft ? Colors.blueGrey.shade800 : Colors.white; final textColor = isDraft ? Colors.white : Colors.black87; + final locked = _currentInvoice.isLocked; + return Scaffold( backgroundColor: themeColor, appBar: AppBar( @@ -157,6 +159,15 @@ class _InvoiceDetailPageState extends State { title: Text(isDraft ? "伝票詳細 (下書き)" : "販売アシスト1号 伝票詳細"), backgroundColor: isDraft ? Colors.black87 : Colors.blueGrey, actions: [ + if (locked) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: Chip( + label: const Text("ロック中", style: TextStyle(color: Colors.white)), + avatar: const Icon(Icons.lock, size: 16, color: Colors.white), + backgroundColor: Colors.redAccent, + ), + ), if (isDraft && !_isEditing) TextButton.icon( icon: const Icon(Icons.check_circle_outline, color: Colors.orangeAccent), @@ -165,50 +176,42 @@ class _InvoiceDetailPageState extends State { ), if (!_isEditing) ...[ IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"), - if (widget.isUnlocked) + if (widget.isUnlocked && !locked) IconButton( icon: const Icon(Icons.copy), tooltip: "コピーして新規作成", onPressed: () async { - // 新しいIDを生成して複製 final newId = DateTime.now().millisecondsSinceEpoch.toString(); final duplicateInvoice = _currentInvoice.copyWith( id: newId, date: DateTime.now(), - isDraft: true, // 下書きとして開始 + isDraft: true, ); - await Navigator.push( context, MaterialPageRoute( builder: (context) => InvoiceInputForm( - onInvoiceGenerated: (inv, path) { - // ここでは特に何もしない(詳細画面は元の伝票を表示し続けるため) - // ただし、履歴画面に戻った時にリロードされる必要がある(HistoryScreen側で対応済み) - }, + onInvoiceGenerated: (inv, path) {}, existingInvoice: duplicateInvoice, ), ), ); }, ), - if (widget.isUnlocked) + if (widget.isUnlocked && !locked) IconButton( - icon: const Icon(Icons.edit_note), // アイコン変更 + icon: const Icon(Icons.edit_note), tooltip: "詳細編集", onPressed: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) => InvoiceInputForm( - onInvoiceGenerated: (inv, path) { - // 保存完了時のコールバック(必要なら) - }, + onInvoiceGenerated: (inv, path) {}, existingInvoice: _currentInvoice, ), ), ); - // 戻ってきたらデータを再読み込み(リポジトリから取得) final repo = InvoiceRepository(); final customerRepo = CustomerRepository(); final customers = await customerRepo.getAllCustomers(); diff --git a/lib/screens/invoice_history_screen.dart b/lib/screens/invoice_history_screen.dart index 7d50d6b..55c2df0 100644 --- a/lib/screens/invoice_history_screen.dart +++ b/lib/screens/invoice_history_screen.dart @@ -297,17 +297,26 @@ class _InvoiceHistoryScreenState extends State { backgroundColor: invoice.isDraft ? Colors.orange.shade100 : (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200), - child: Icon( - invoice.isDraft ? Icons.edit_note : Icons.description_outlined, - color: invoice.isDraft - ? Colors.orange - : (_isUnlocked ? Colors.indigo : Colors.grey), + child: Stack( + children: [ + Align( + alignment: Alignment.center, + child: Icon( + invoice.isDraft ? Icons.edit_note : Icons.description_outlined, + color: invoice.isDraft + ? Colors.orange + : (_isUnlocked ? Colors.indigo : Colors.grey), + ), + ), + if (invoice.isLocked) + const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)), + ], ), ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(invoice.customerNameForDisplay, style: const TextStyle(fontWeight: FontWeight.bold)), + Text(invoice.customerNameForDisplay, style: TextStyle(fontWeight: FontWeight.bold, color: invoice.isLocked ? Colors.grey : Colors.black87)), if (invoice.subject?.isNotEmpty ?? false) Text( invoice.subject!, @@ -343,6 +352,10 @@ class _InvoiceHistoryScreenState extends State { _loadData(); }, onLongPress: () async { + if (invoice.isLocked) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ロック中の伝票は削除できません"))); + return; + } if (!_isUnlocked) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("削除するにはアンロックが必要です")), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 9b1ac86..d998478 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,103 +1,228 @@ import 'package:flutter/material.dart'; import 'company_info_screen.dart'; -class SettingsScreen extends StatelessWidget { +class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); - void _showPlaceholder(BuildContext context, String title) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$title の設定は後で追加してください')), - ); + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + // Company + final _companyNameCtrl = TextEditingController(); + final _companyZipCtrl = TextEditingController(); + final _companyAddrCtrl = TextEditingController(); + final _companyTelCtrl = TextEditingController(); + final _companyRegCtrl = TextEditingController(); + + // Staff + final _staffNameCtrl = TextEditingController(); + final _staffMailCtrl = TextEditingController(); + + // SMTP + final _smtpHostCtrl = TextEditingController(); + final _smtpPortCtrl = TextEditingController(text: '587'); + final _smtpUserCtrl = TextEditingController(); + final _smtpPassCtrl = TextEditingController(); + bool _smtpTls = true; + + // Backup + final _backupPathCtrl = TextEditingController(); + + String _theme = 'system'; + + @override + void dispose() { + _companyNameCtrl.dispose(); + _companyZipCtrl.dispose(); + _companyAddrCtrl.dispose(); + _companyTelCtrl.dispose(); + _companyRegCtrl.dispose(); + _staffNameCtrl.dispose(); + _staffMailCtrl.dispose(); + _smtpHostCtrl.dispose(); + _smtpPortCtrl.dispose(); + _smtpUserCtrl.dispose(); + _smtpPassCtrl.dispose(); + _backupPathCtrl.dispose(); + super.dispose(); } - void _showThemePicker(BuildContext context) { - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), - builder: (context) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('テーマ選択', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 12), - ListTile( - leading: const Icon(Icons.brightness_5), - title: const Text('ライト'), - onTap: () { - Navigator.pop(context); - _showPlaceholder(context, 'ライトテーマ適用'); - }, - ), - ListTile( - leading: const Icon(Icons.brightness_3), - title: const Text('ダーク'), - onTap: () { - Navigator.pop(context); - _showPlaceholder(context, 'ダークテーマ適用'); - }, - ), - ListTile( - leading: const Icon(Icons.brightness_auto), - title: const Text('システムに従う'), - onTap: () { - Navigator.pop(context); - _showPlaceholder(context, 'システムテーマ適用'); - }, - ), - ], - ), - ), - ); + void _showSnackbar(String msg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } + void _saveCompany() => _showSnackbar('自社情報を保存(テンプレ)'); + void _saveStaff() => _showSnackbar('担当者情報を保存(テンプレ)'); + void _saveSmtp() => _showSnackbar('SMTP設定を保存(テンプレ)'); + void _saveBackup() => _showSnackbar('バックアップ設定を保存(テンプレ)'); + + void _pickBackupPath() => _showSnackbar('バックアップ先の選択は後で実装'); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('設定'), + actions: [ + IconButton( + icon: const Icon(Icons.info_outline), + onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'), + ) + ], ), body: ListView( + padding: const EdgeInsets.all(16), children: [ - ListTile( - leading: const Icon(Icons.business), - title: const Text('自社情報'), - subtitle: const Text('会社名・住所・登録番号など'), - onTap: () async { - await Navigator.push(context, MaterialPageRoute(builder: (context) => const CompanyInfoScreen())); - }, + _section( + title: '自社情報', + subtitle: '会社名・住所・登録番号など', + child: Column( + children: [ + TextField(controller: _companyNameCtrl, decoration: const InputDecoration(labelText: '会社名')), + TextField(controller: _companyZipCtrl, decoration: const InputDecoration(labelText: '郵便番号')), + TextField(controller: _companyAddrCtrl, decoration: const InputDecoration(labelText: '住所')), + TextField(controller: _companyTelCtrl, decoration: const InputDecoration(labelText: '電話番号')), + TextField(controller: _companyRegCtrl, decoration: const InputDecoration(labelText: '登録番号 (インボイス)')), + const SizedBox(height: 8), + Row( + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.upload_file), + label: const Text('画面で編集'), + onPressed: () async { + await Navigator.push(context, MaterialPageRoute(builder: (context) => const CompanyInfoScreen())); + }, + ), + const SizedBox(width: 8), + ElevatedButton.icon( + icon: const Icon(Icons.save), + label: const Text('保存'), + onPressed: _saveCompany, + ), + ], + ), + ], + ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.badge_outlined), - title: const Text('担当者情報'), - subtitle: const Text('自社担当者の署名・連絡先'), - onTap: () => _showPlaceholder(context, '担当者情報'), + _section( + title: '担当者情報', + subtitle: '署名や連絡先(送信者情報)', + child: Column( + children: [ + TextField(controller: _staffNameCtrl, decoration: const InputDecoration(labelText: '担当者名')), + TextField(controller: _staffMailCtrl, decoration: const InputDecoration(labelText: 'メールアドレス')), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.save), + label: const Text('保存'), + onPressed: _saveStaff, + ), + ], + ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.email_outlined), - title: const Text('SMTP情報'), - subtitle: const Text('メール送信サーバ設定'), - onTap: () => _showPlaceholder(context, 'SMTP情報'), + _section( + title: 'SMTP情報', + subtitle: 'メール送信サーバ設定(テンプレ)', + child: Column( + children: [ + TextField(controller: _smtpHostCtrl, decoration: const InputDecoration(labelText: 'ホスト名')), + TextField(controller: _smtpPortCtrl, decoration: const InputDecoration(labelText: 'ポート番号'), keyboardType: TextInputType.number), + TextField(controller: _smtpUserCtrl, decoration: const InputDecoration(labelText: 'ユーザー名')), + TextField(controller: _smtpPassCtrl, decoration: const InputDecoration(labelText: 'パスワード'), obscureText: true), + SwitchListTile( + title: const Text('STARTTLS を使用'), + value: _smtpTls, + onChanged: (v) => setState(() => _smtpTls = v), + ), + ElevatedButton.icon( + icon: const Icon(Icons.save), + label: const Text('保存'), + onPressed: _saveSmtp, + ), + ], + ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.cloud_upload_outlined), - title: const Text('バックアップドライブ'), - subtitle: const Text('バックアップ先のクラウド/ローカルドライブ'), - onTap: () => _showPlaceholder(context, 'バックアップドライブ'), + _section( + title: 'バックアップドライブ', + subtitle: 'バックアップ先のクラウド/ローカル', + child: Column( + children: [ + TextField(controller: _backupPathCtrl, decoration: const InputDecoration(labelText: '保存先パス/URL')), + const SizedBox(height: 8), + Row( + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.folder_open), + label: const Text('参照'), + onPressed: _pickBackupPath, + ), + const SizedBox(width: 8), + ElevatedButton.icon( + icon: const Icon(Icons.save), + label: const Text('保存'), + onPressed: _saveBackup, + ), + ], + ), + ], + ), ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.palette_outlined), - title: const Text('テーマ選択'), - subtitle: const Text('配色や見た目を切り替え'), - onTap: () => _showThemePicker(context), + _section( + title: 'テーマ選択', + subtitle: '配色や見た目を切り替え(テンプレ)', + child: Column( + children: [ + RadioListTile( + value: 'light', + groupValue: _theme, + title: const Text('ライト'), + onChanged: (v) => setState(() => _theme = v ?? 'light'), + ), + RadioListTile( + value: 'dark', + groupValue: _theme, + title: const Text('ダーク'), + onChanged: (v) => setState(() => _theme = v ?? 'dark'), + ), + RadioListTile( + value: 'system', + groupValue: _theme, + title: const Text('システムに従う'), + onChanged: (v) => setState(() => _theme = v ?? 'system'), + ), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.save), + label: const Text('保存'), + onPressed: () => _showSnackbar('テーマ設定を保存(テンプレ): $_theme'), + ), + ], + ), ), ], ), ); } + + Widget _section({required String title, required String subtitle, required Widget child}) { + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(subtitle, style: const TextStyle(color: Colors.grey)), + const SizedBox(height: 12), + child, + ], + ), + ), + ); + } }