chore: flesh out settings template and lock badges

This commit is contained in:
joe 2026-02-25 19:53:33 +09:00
parent 60fc0b46ac
commit bab533fccb
3 changed files with 237 additions and 96 deletions

View file

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

View file

@ -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("削除するにはアンロックが必要です")),

View file

@ -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,
],
),
),
);
}
}