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 themeColor = isDraft ? Colors.blueGrey.shade800 : Colors.white;
|
||||||
final textColor = isDraft ? Colors.white : Colors.black87;
|
final textColor = isDraft ? Colors.white : Colors.black87;
|
||||||
|
|
||||||
|
final locked = _currentInvoice.isLocked;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: themeColor,
|
backgroundColor: themeColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -157,6 +159,15 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
title: Text(isDraft ? "伝票詳細 (下書き)" : "販売アシスト1号 伝票詳細"),
|
title: Text(isDraft ? "伝票詳細 (下書き)" : "販売アシスト1号 伝票詳細"),
|
||||||
backgroundColor: isDraft ? Colors.black87 : Colors.blueGrey,
|
backgroundColor: isDraft ? Colors.black87 : Colors.blueGrey,
|
||||||
actions: [
|
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)
|
if (isDraft && !_isEditing)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.check_circle_outline, color: Colors.orangeAccent),
|
icon: const Icon(Icons.check_circle_outline, color: Colors.orangeAccent),
|
||||||
|
|
@ -165,50 +176,42 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
),
|
),
|
||||||
if (!_isEditing) ...[
|
if (!_isEditing) ...[
|
||||||
IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
|
IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
|
||||||
if (widget.isUnlocked)
|
if (widget.isUnlocked && !locked)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.copy),
|
icon: const Icon(Icons.copy),
|
||||||
tooltip: "コピーして新規作成",
|
tooltip: "コピーして新規作成",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// 新しいIDを生成して複製
|
|
||||||
final newId = DateTime.now().millisecondsSinceEpoch.toString();
|
final newId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
final duplicateInvoice = _currentInvoice.copyWith(
|
final duplicateInvoice = _currentInvoice.copyWith(
|
||||||
id: newId,
|
id: newId,
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
isDraft: true, // 下書きとして開始
|
isDraft: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => InvoiceInputForm(
|
builder: (context) => InvoiceInputForm(
|
||||||
onInvoiceGenerated: (inv, path) {
|
onInvoiceGenerated: (inv, path) {},
|
||||||
// ここでは特に何もしない(詳細画面は元の伝票を表示し続けるため)
|
|
||||||
// ただし、履歴画面に戻った時にリロードされる必要がある(HistoryScreen側で対応済み)
|
|
||||||
},
|
|
||||||
existingInvoice: duplicateInvoice,
|
existingInvoice: duplicateInvoice,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.isUnlocked)
|
if (widget.isUnlocked && !locked)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit_note), // アイコン変更
|
icon: const Icon(Icons.edit_note),
|
||||||
tooltip: "詳細編集",
|
tooltip: "詳細編集",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => InvoiceInputForm(
|
builder: (context) => InvoiceInputForm(
|
||||||
onInvoiceGenerated: (inv, path) {
|
onInvoiceGenerated: (inv, path) {},
|
||||||
// 保存完了時のコールバック(必要なら)
|
|
||||||
},
|
|
||||||
existingInvoice: _currentInvoice,
|
existingInvoice: _currentInvoice,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// 戻ってきたらデータを再読み込み(リポジトリから取得)
|
|
||||||
final repo = InvoiceRepository();
|
final repo = InvoiceRepository();
|
||||||
final customerRepo = CustomerRepository();
|
final customerRepo = CustomerRepository();
|
||||||
final customers = await customerRepo.getAllCustomers();
|
final customers = await customerRepo.getAllCustomers();
|
||||||
|
|
|
||||||
|
|
@ -297,17 +297,26 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
backgroundColor: invoice.isDraft
|
backgroundColor: invoice.isDraft
|
||||||
? Colors.orange.shade100
|
? Colors.orange.shade100
|
||||||
: (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
|
: (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
|
||||||
child: Icon(
|
child: Stack(
|
||||||
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
|
children: [
|
||||||
color: invoice.isDraft
|
Align(
|
||||||
? Colors.orange
|
alignment: Alignment.center,
|
||||||
: (_isUnlocked ? Colors.indigo : Colors.grey),
|
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(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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)
|
if (invoice.subject?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
invoice.subject!,
|
invoice.subject!,
|
||||||
|
|
@ -343,6 +352,10 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
_loadData();
|
_loadData();
|
||||||
},
|
},
|
||||||
onLongPress: () async {
|
onLongPress: () async {
|
||||||
|
if (invoice.isLocked) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ロック中の伝票は削除できません")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_isUnlocked) {
|
if (!_isUnlocked) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text("削除するにはアンロックが必要です")),
|
const SnackBar(content: Text("削除するにはアンロックが必要です")),
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,228 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'company_info_screen.dart';
|
import 'company_info_screen.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
void _showPlaceholder(BuildContext context, String title) {
|
@override
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
SnackBar(content: Text('$title の設定は後で追加してください')),
|
}
|
||||||
);
|
|
||||||
|
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 _showThemePicker(BuildContext context) {
|
void _showSnackbar(String msg) {
|
||||||
showModalBottomSheet(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
|
||||||
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 _saveCompany() => _showSnackbar('自社情報を保存(テンプレ)');
|
||||||
|
void _saveStaff() => _showSnackbar('担当者情報を保存(テンプレ)');
|
||||||
|
void _saveSmtp() => _showSnackbar('SMTP設定を保存(テンプレ)');
|
||||||
|
void _saveBackup() => _showSnackbar('バックアップ設定を保存(テンプレ)');
|
||||||
|
|
||||||
|
void _pickBackupPath() => _showSnackbar('バックアップ先の選択は後で実装');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('設定'),
|
title: const Text('設定'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
_section(
|
||||||
leading: const Icon(Icons.business),
|
title: '自社情報',
|
||||||
title: const Text('自社情報'),
|
subtitle: '会社名・住所・登録番号など',
|
||||||
subtitle: const Text('会社名・住所・登録番号など'),
|
child: Column(
|
||||||
onTap: () async {
|
children: [
|
||||||
await Navigator.push(context, MaterialPageRoute(builder: (context) => const CompanyInfoScreen()));
|
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),
|
_section(
|
||||||
ListTile(
|
title: '担当者情報',
|
||||||
leading: const Icon(Icons.badge_outlined),
|
subtitle: '署名や連絡先(送信者情報)',
|
||||||
title: const Text('担当者情報'),
|
child: Column(
|
||||||
subtitle: const Text('自社担当者の署名・連絡先'),
|
children: [
|
||||||
onTap: () => _showPlaceholder(context, '担当者情報'),
|
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),
|
_section(
|
||||||
ListTile(
|
title: 'SMTP情報',
|
||||||
leading: const Icon(Icons.email_outlined),
|
subtitle: 'メール送信サーバ設定(テンプレ)',
|
||||||
title: const Text('SMTP情報'),
|
child: Column(
|
||||||
subtitle: const Text('メール送信サーバ設定'),
|
children: [
|
||||||
onTap: () => _showPlaceholder(context, 'SMTP情報'),
|
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),
|
_section(
|
||||||
ListTile(
|
title: 'バックアップドライブ',
|
||||||
leading: const Icon(Icons.cloud_upload_outlined),
|
subtitle: 'バックアップ先のクラウド/ローカル',
|
||||||
title: const Text('バックアップドライブ'),
|
child: Column(
|
||||||
subtitle: const Text('バックアップ先のクラウド/ローカルドライブ'),
|
children: [
|
||||||
onTap: () => _showPlaceholder(context, 'バックアップドライブ'),
|
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),
|
_section(
|
||||||
ListTile(
|
title: 'テーマ選択',
|
||||||
leading: const Icon(Icons.palette_outlined),
|
subtitle: '配色や見た目を切り替え(テンプレ)',
|
||||||
title: const Text('テーマ選択'),
|
child: Column(
|
||||||
subtitle: const Text('配色や見た目を切り替え'),
|
children: [
|
||||||
onTap: () => _showThemePicker(context),
|
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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