feat: add settings menu and explorer-style master UIs

This commit is contained in:
joe 2026-02-25 19:46:54 +09:00
parent 145f0d7cad
commit 60fc0b46ac
6 changed files with 484 additions and 236 deletions

View file

@ -12,8 +12,11 @@ class CustomerMasterScreen extends StatefulWidget {
class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final CustomerRepository _customerRepo = CustomerRepository();
final TextEditingController _searchController = TextEditingController();
List<Customer> _customers = [];
List<Customer> _filtered = [];
bool _isLoading = true;
String _sortKey = 'name_asc';
@override
void initState() {
@ -26,10 +29,26 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final customers = await _customerRepo.getAllCustomers();
setState(() {
_customers = customers;
_applyFilter();
_isLoading = false;
});
}
void _applyFilter() {
final query = _searchController.text.toLowerCase();
List<Customer> list = _customers.where((c) {
return c.displayName.toLowerCase().contains(query) || c.formalName.toLowerCase().contains(query);
}).toList();
switch (_sortKey) {
case 'name_desc':
list.sort((a, b) => b.displayName.compareTo(a.displayName));
break;
default:
list.sort((a, b) => a.displayName.compareTo(b.displayName));
}
_filtered = list;
}
Future<void> _addOrEditCustomer({Customer? customer}) async {
final isEdit = customer != null;
final displayNameController = TextEditingController(text: customer?.displayName ?? "");
@ -116,49 +135,81 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return Scaffold(
appBar: AppBar(
leading: const BackButton(),
title: const Text("顧客マスター管理"),
backgroundColor: Colors.blueGrey,
title: const Text("顧客マスター"),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _sortKey,
icon: const Icon(Icons.sort, color: Colors.white),
dropdownColor: Colors.white,
items: const [
DropdownMenuItem(value: 'name_asc', child: Text('名前昇順')),
DropdownMenuItem(value: 'name_desc', child: Text('名前降順')),
],
onChanged: (v) {
setState(() {
_sortKey = v ?? 'name_asc';
_applyFilter();
});
},
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadCustomers,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _customers.isEmpty
? const Center(child: Text("顧客が登録されていません"))
: ListView.builder(
itemCount: _customers.length,
itemBuilder: (context, index) {
final c = _customers[index];
return ListTile(
title: Text(c.displayName),
subtitle: Text("${c.formalName} ${c.title}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _addOrEditCustomer(customer: c)),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除確認"),
content: Text("${c.displayName}」を削除しますか?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
await _customerRepo.deleteCustomer(c.id);
_loadCustomers();
}
},
),
],
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "名前で検索 (電話帳参照ボタンは詳細で)",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
),
onChanged: (_) => setState(_applyFilter),
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filtered.isEmpty
? const Center(child: Text("顧客が登録されていません"))
: ListView.builder(
itemCount: _filtered.length,
itemBuilder: (context, index) {
final c = _filtered[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: c.isLocked ? Colors.grey.shade300 : Colors.indigo.shade100,
child: Stack(
children: [
const Align(alignment: Alignment.center, child: Icon(Icons.person, color: Colors.indigo)),
if (c.isLocked)
const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)),
],
),
),
title: Text(c.displayName, style: TextStyle(fontWeight: FontWeight.bold, color: c.isLocked ? Colors.grey : Colors.black87)),
subtitle: Text("${c.formalName} ${c.title}"),
onTap: () => _showDetailPane(c),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: c.isLocked ? null : () => _addOrEditCustomer(customer: c),
tooltip: c.isLocked ? "ロック中" : "編集",
),
);
},
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addOrEditCustomer(),
child: const Icon(Icons.person_add),
@ -166,4 +217,87 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
),
);
}
void _showDetailPane(Customer c) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.5,
maxChildSize: 0.8,
minChildSize: 0.4,
expand: false,
builder: (context, scrollController) => Padding(
padding: const EdgeInsets.all(16),
child: ListView(
controller: scrollController,
children: [
Row(
children: [
Icon(c.isLocked ? Icons.lock : Icons.person, color: c.isLocked ? Colors.redAccent : Colors.indigo),
const SizedBox(width: 8),
Expanded(child: Text(c.formalName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
IconButton(
icon: const Icon(Icons.call),
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("電話帳参照は端末連絡先連携が必要です")));
},
tooltip: "電話帳参照",
),
],
),
const SizedBox(height: 8),
if (c.address != null) Text("住所: ${c.address}") else const SizedBox.shrink(),
if (c.tel != null) Text("TEL: ${c.tel}") else const SizedBox.shrink(),
Text("敬称: ${c.title}"),
const SizedBox(height: 12),
Row(
children: [
OutlinedButton.icon(
onPressed: () {
Navigator.pop(context);
_addOrEditCustomer(customer: c);
},
icon: const Icon(Icons.edit),
label: const Text("編集"),
),
const SizedBox(width: 8),
if (!c.isLocked)
OutlinedButton.icon(
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除確認"),
content: Text("${c.displayName}」を削除しますか?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
await _customerRepo.deleteCustomer(c.id);
if (!mounted) return;
Navigator.pop(context);
_loadCustomers();
}
},
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
label: const Text("削除", style: TextStyle(color: Colors.redAccent)),
),
if (c.isLocked)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Chip(label: const Text("ロック中"), avatar: const Icon(Icons.lock, size: 16)),
),
],
),
],
),
),
),
);
}
}

View file

@ -6,6 +6,9 @@ import '../services/invoice_repository.dart';
import '../services/customer_repository.dart';
import 'invoice_detail_page.dart';
import 'management_screen.dart';
import 'product_master_screen.dart';
import 'customer_master_screen.dart';
import 'settings_screen.dart';
import 'company_info_screen.dart';
import '../widgets/slide_to_unlock.dart';
import '../main.dart'; // InvoiceFlowScreen
@ -98,6 +101,63 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
final dateFormatter = DateFormat('yyyy/MM/dd');
return Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.indigo.shade700),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text("メニュー", style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text("v$_appVersion", style: const TextStyle(color: Colors.white70)),
],
),
),
ListTile(
leading: const Icon(Icons.receipt_long),
title: const Text("伝票マスター"),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.people),
title: const Text("顧客マスター"),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomerMasterScreen()));
},
),
ListTile(
leading: const Icon(Icons.inventory_2),
title: const Text("商品マスター"),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductMasterScreen()));
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text("設定"),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()));
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: const Text("管理メニュー"),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (_) => const ManagementScreen()));
},
),
],
),
),
appBar: AppBar(
// leading removed
title: GestureDetector(

View file

@ -32,11 +32,11 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
Customer? _selectedCustomer;
final List<InvoiceItem> _items = [];
double _taxRate = 0.10;
bool _includeTax = true;
bool _includeTax = false;
CompanyInfo? _companyInfo;
DocumentType _documentType = DocumentType.invoice; //
DateTime _selectedDate = DateTime.now(); // :
bool _isDraft = false; // :
bool _isDraft = true; //
final TextEditingController _subjectController = TextEditingController(); //
bool _isSaving = false; //
String _status = "取引先と商品を入力してください";
@ -74,7 +74,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
_isDraft = inv.isDraft;
if (inv.subject != null) _subjectController.text = inv.subject!;
} else {
_taxRate = companyInfo.defaultTaxRate;
_taxRate = 0;
_includeTax = false;
_isDraft = true;
}
});
}
@ -202,15 +204,14 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
@override
Widget build(BuildContext context) {
final fmt = NumberFormat("#,###");
final themeColor = _isDraft ? Colors.blueGrey.shade800 : Colors.white;
final textColor = _isDraft ? Colors.white : Colors.black87;
final themeColor = Colors.white;
final textColor = Colors.black87;
return Scaffold(
backgroundColor: themeColor,
appBar: AppBar(
leading: const BackButton(),
title: Text(_isDraft ? "伝票作成 (下書き)" : "販売アシスト1号 V1.5.06"),
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
title: const Text("販売アシスト1号 V1.5.06"),
),
body: Stack(
children: [
@ -222,8 +223,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDraftToggle(),
const SizedBox(height: 16),
_buildDocumentTypeSection(),
const SizedBox(height: 16),
_buildDateSection(),
@ -234,8 +233,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
const SizedBox(height: 20),
_buildItemsSection(fmt),
const SizedBox(height: 20),
_buildTaxSettings(),
const SizedBox(height: 20),
_buildSummarySection(fmt),
const SizedBox(height: 20),
_buildSignatureSection(),
@ -265,73 +262,36 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
);
}
Widget _buildDocumentTypeSection() {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: DocumentType.values.map((type) {
final isSelected = _documentType == type;
String label = "";
IconData icon = Icons.description;
switch (type) {
case DocumentType.estimation: label = "見積"; icon = Icons.article_outlined; break;
case DocumentType.delivery: label = "納品"; icon = Icons.local_shipping_outlined; break;
case DocumentType.invoice: label = "請求"; icon = Icons.receipt_long_outlined; break;
case DocumentType.receipt: label = "領収"; icon = Icons.payments_outlined; break;
}
return Expanded(
child: InkWell(
onTap: () => setState(() => _documentType = type),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? Colors.indigo : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(icon, color: isSelected ? Colors.white : Colors.grey.shade600, size: 20),
Text(label, style: TextStyle(
color: isSelected ? Colors.white : Colors.grey.shade600,
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
)),
],
),
),
),
);
}).toList(),
),
);
}
Widget _buildDateSection() {
final fmt = DateFormat('yyyy年MM月dd日');
return Card(
elevation: 0,
color: Colors.blueGrey.shade50,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: const Icon(Icons.calendar_today, color: Colors.blueGrey),
title: Text("伝票日付: ${fmt.format(_selectedDate)}", style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: const Text("タップして日付を変更"),
trailing: const Icon(Icons.edit, size: 20),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
},
final fmt = DateFormat('yyyy/MM/dd');
return GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() => _selectedDate = picked);
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
children: [
const Icon(Icons.calendar_today, size: 18, color: Colors.indigo),
const SizedBox(width: 8),
Text("伝票日付: ${fmt.format(_selectedDate)}", style: const TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
const Icon(Icons.chevron_right, size: 18, color: Colors.indigo),
],
),
),
);
}
@ -477,55 +437,15 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
);
}
Widget _buildTaxSettings() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (_includeTax) ...[
const Text("消費税率: ", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
ChoiceChip(
label: const Text("10%"),
selected: _taxRate == 0.10,
onSelected: (val) => setState(() => _taxRate = 0.10),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text("8%"),
selected: _taxRate == 0.08,
onSelected: (val) => setState(() => _taxRate = 0.08),
),
] else
const Text("消費税設定: 非課税", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
const Spacer(),
Switch(
value: _includeTax,
onChanged: (val) => setState(() => _includeTax = val),
),
Text(_includeTax ? "税込計算" : "非課税"),
],
),
],
);
}
Widget _buildSummarySection(NumberFormat fmt) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.indigo.shade900, borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
_buildSummaryRow(_includeTax ? "小計 (税抜)" : "小計", "${fmt.format(_subTotal)}", Colors.white70),
if (_includeTax) ...[
if (_companyInfo?.taxDisplayMode == 'normal')
_buildSummaryRow("消費税 (${(_taxRate * 100).toInt()}%)", "${fmt.format(_tax)}", Colors.white70),
if (_companyInfo?.taxDisplayMode == 'text_only')
_buildSummaryRow("消費税", "(税別)", Colors.white70),
],
_buildSummaryRow("小計", "${fmt.format(_subTotal)}", Colors.white70),
const Divider(color: Colors.white24),
_buildSummaryRow(_includeTax ? "合計金額 (税込)" : "合計金額", "${fmt.format(_total)}", Colors.white, fontSize: 24),
_buildSummaryRow("合計金額", "${fmt.format(_subTotal)}", Colors.white, fontSize: 24),
],
),
);
@ -596,7 +516,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _showPreview,
onPressed: _items.isEmpty ? null : _showPreview,
icon: const Icon(Icons.picture_as_pdf), //
label: const Text("PDFプレビュー"), //
style: OutlinedButton.styleFrom(
@ -610,9 +530,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
child: ElevatedButton.icon(
onPressed: () => _saveInvoice(generatePdf: false),
icon: const Icon(Icons.save),
label: const Text("保存のみ"),
label: const Text("保存"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey,
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
@ -620,52 +540,12 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
),
],
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () => _saveInvoice(generatePdf: true),
icon: const Icon(Icons.picture_as_pdf),
label: const Text("確定してPDF生成"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
),
);
}
Widget _buildDraftToggle() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: _isDraft ? Colors.black26 : Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _isDraft ? Colors.orangeAccent : Colors.orange, width: 2),
),
child: Row(
children: [
Icon(_isDraft ? Icons.drafts : Icons.check_circle, color: Colors.orange),
const SizedBox(width: 12),
Expanded(
child: Text(
_isDraft ? "下書き (保存のみ・PDF未生成)" : "正式発行 (PDF生成)",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: _isDraft ? Colors.white70 : Colors.orange.shade900),
),
),
Switch(
value: _isDraft,
activeColor: Colors.orangeAccent,
onChanged: (val) => setState(() => _isDraft = val),
),
],
),
);
}
Widget _buildSubjectSection(Color textColor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View file

@ -159,37 +159,23 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
itemBuilder: (context, index) {
final p = _filteredProducts[index];
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.inventory_2)),
title: Text(p.name),
leading: CircleAvatar(
backgroundColor: p.isLocked ? Colors.grey.shade300 : Colors.indigo.shade100,
child: Stack(
children: [
const Align(alignment: Alignment.center, child: Icon(Icons.inventory_2, color: Colors.indigo)),
if (p.isLocked)
const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)),
],
),
),
title: Text(p.name, style: TextStyle(fontWeight: FontWeight.bold, color: p.isLocked ? Colors.grey : Colors.black87)),
subtitle: Text("${p.category ?? '未分類'} - ¥${p.defaultUnitPrice} (在庫: ${p.stockQuantity})"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _showEditDialog(product: p)),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除の確認"),
content: Text("${p.name}を削除してよろしいですか?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () async {
await _productRepo.deleteProduct(p.id);
Navigator.pop(context);
_loadProducts();
},
child: const Text("削除", style: TextStyle(color: Colors.red)),
),
],
),
);
},
),
],
onTap: () => _showDetailPane(p),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: p.isLocked ? null : () => _showEditDialog(product: p),
tooltip: p.isLocked ? "ロック中" : "編集",
),
);
},
@ -202,4 +188,83 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
),
);
}
void _showDetailPane(Product p) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.45,
maxChildSize: 0.8,
minChildSize: 0.35,
expand: false,
builder: (context, scrollController) => Padding(
padding: const EdgeInsets.all(16),
child: ListView(
controller: scrollController,
children: [
Row(
children: [
Icon(p.isLocked ? Icons.lock : Icons.inventory_2, color: p.isLocked ? Colors.redAccent : Colors.indigo),
const SizedBox(width: 8),
Expanded(child: Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
Chip(label: Text(p.category ?? '未分類')),
],
),
const SizedBox(height: 8),
Text("単価: ¥${p.defaultUnitPrice}"),
Text("在庫: ${p.stockQuantity}"),
if (p.barcode != null && p.barcode!.isNotEmpty) Text("バーコード: ${p.barcode}"),
const SizedBox(height: 12),
Row(
children: [
OutlinedButton.icon(
onPressed: () {
Navigator.pop(context);
_showEditDialog(product: p);
},
icon: const Icon(Icons.edit),
label: const Text("編集"),
),
const SizedBox(width: 8),
if (!p.isLocked)
OutlinedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除の確認"),
content: Text("${p.name}を削除してよろしいですか?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () async {
await _productRepo.deleteProduct(p.id);
if (!mounted) return;
Navigator.pop(context); // dialog
Navigator.pop(context); // sheet
_loadProducts();
},
child: const Text("削除", style: TextStyle(color: Colors.red)),
),
],
),
);
},
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
label: const Text("削除", style: TextStyle(color: Colors.redAccent)),
),
if (p.isLocked)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Chip(label: const Text("ロック中"), avatar: const Icon(Icons.lock, size: 16)),
),
],
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'company_info_screen.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
void _showPlaceholder(BuildContext context, String title) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$title の設定は後で追加してください')),
);
}
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, 'システムテーマ適用');
},
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('設定'),
),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.business),
title: const Text('自社情報'),
subtitle: const Text('会社名・住所・登録番号など'),
onTap: () 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 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, 'バックアップドライブ'),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.palette_outlined),
title: const Text('テーマ選択'),
subtitle: const Text('配色や見た目を切り替え'),
onTap: () => _showThemePicker(context),
),
],
),
);
}
}

View file

@ -16,8 +16,7 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
//
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final boldTtf = pw.Font.ttf(fontData);
final ipaex = pw.Font.ttf(fontData);
final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("#,###");
@ -40,7 +39,14 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
theme: pw.ThemeData.withFont(base: ttf, bold: boldTtf),
theme: pw.ThemeData.withFont(
base: ipaex,
bold: ipaex,
italic: ipaex,
boldItalic: ipaex,
).copyWith(
defaultTextStyle: pw.TextStyle(fontFallback: [ipaex]),
),
build: (context) => [
//
pw.Header(