chore: flesh out settings template and lock badges
This commit is contained in:
parent
60fc0b46ac
commit
bab533fccb
3 changed files with 237 additions and 96 deletions
|
|
@ -150,6 +150,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
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<InvoiceDetailPage> {
|
|||
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<InvoiceDetailPage> {
|
|||
),
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -297,6 +297,10 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
backgroundColor: invoice.isDraft
|
||||
? Colors.orange.shade100
|
||||
: (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
|
||||
color: invoice.isDraft
|
||||
|
|
@ -304,10 +308,15 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
: (_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<InvoiceHistoryScreen> {
|
|||
_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("削除するにはアンロックが必要です")),
|
||||
|
|
|
|||
|
|
@ -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<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
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, 'システムテーマ適用');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// 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 _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 {
|
||||
_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 Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.badge_outlined),
|
||||
title: const Text('担当者情報'),
|
||||
subtitle: const Text('自社担当者の署名・連絡先'),
|
||||
onTap: () => _showPlaceholder(context, '担当者情報'),
|
||||
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.email_outlined),
|
||||
title: const Text('SMTP情報'),
|
||||
subtitle: const Text('メール送信サーバ設定'),
|
||||
onTap: () => _showPlaceholder(context, 'SMTP情報'),
|
||||
],
|
||||
),
|
||||
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: _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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_section(
|
||||
title: 'テーマ選択',
|
||||
subtitle: '配色や見た目を切り替え(テンプレ)',
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
value: 'light',
|
||||
groupValue: _theme,
|
||||
title: const Text('ライト'),
|
||||
onChanged: (v) => setState(() => _theme = v ?? 'light'),
|
||||
),
|
||||
RadioListTile<String>(
|
||||
value: 'dark',
|
||||
groupValue: _theme,
|
||||
title: const Text('ダーク'),
|
||||
onChanged: (v) => setState(() => _theme = v ?? 'dark'),
|
||||
),
|
||||
RadioListTile<String>(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette_outlined),
|
||||
title: const Text('テーマ選択'),
|
||||
subtitle: const Text('配色や見た目を切り替え'),
|
||||
onTap: () => _showThemePicker(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue