From 60fc0b46acde345045935a6977709b1156d40581 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 25 Feb 2026 19:46:54 +0900 Subject: [PATCH] feat: add settings menu and explorer-style master UIs --- lib/screens/customer_master_screen.dart | 216 +++++++++++++++++++----- lib/screens/invoice_history_screen.dart | 60 +++++++ lib/screens/invoice_input_screen.dart | 204 +++++----------------- lib/screens/product_master_screen.dart | 125 ++++++++++---- lib/screens/settings_screen.dart | 103 +++++++++++ lib/services/pdf_generator.dart | 12 +- 6 files changed, 484 insertions(+), 236 deletions(-) create mode 100644 lib/screens/settings_screen.dart diff --git a/lib/screens/customer_master_screen.dart b/lib/screens/customer_master_screen.dart index 69aecbe..b10f1b6 100644 --- a/lib/screens/customer_master_screen.dart +++ b/lib/screens/customer_master_screen.dart @@ -12,8 +12,11 @@ class CustomerMasterScreen extends StatefulWidget { class _CustomerMasterScreenState extends State { final CustomerRepository _customerRepo = CustomerRepository(); + final TextEditingController _searchController = TextEditingController(); List _customers = []; + List _filtered = []; bool _isLoading = true; + String _sortKey = 'name_asc'; @override void initState() { @@ -26,10 +29,26 @@ class _CustomerMasterScreenState extends State { final customers = await _customerRepo.getAllCustomers(); setState(() { _customers = customers; + _applyFilter(); _isLoading = false; }); } + void _applyFilter() { + final query = _searchController.text.toLowerCase(); + List 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 _addOrEditCustomer({Customer? customer}) async { final isEdit = customer != null; final displayNameController = TextEditingController(text: customer?.displayName ?? ""); @@ -116,49 +135,81 @@ class _CustomerMasterScreenState extends State { return Scaffold( appBar: AppBar( leading: const BackButton(), - title: const Text("顧客マスター管理"), - backgroundColor: Colors.blueGrey, + title: const Text("顧客マスター"), + actions: [ + DropdownButtonHideUnderline( + child: DropdownButton( + 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( - 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 { ), ); } + + 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( + 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)), + ), + ], + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/screens/invoice_history_screen.dart b/lib/screens/invoice_history_screen.dart index 269dd11..7d50d6b 100644 --- a/lib/screens/invoice_history_screen.dart +++ b/lib/screens/invoice_history_screen.dart @@ -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 { 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( diff --git a/lib/screens/invoice_input_screen.dart b/lib/screens/invoice_input_screen.dart index 7ffb4eb..492fbb9 100644 --- a/lib/screens/invoice_input_screen.dart +++ b/lib/screens/invoice_input_screen.dart @@ -32,11 +32,11 @@ class _InvoiceInputFormState extends State { Customer? _selectedCustomer; final List _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 { _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 { @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 { 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 { 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 { ); } - 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 { ); } - 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 { 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 { 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 { ), ], ), - 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, diff --git a/lib/screens/product_master_screen.dart b/lib/screens/product_master_screen.dart index 665fb4f..1419e59 100644 --- a/lib/screens/product_master_screen.dart +++ b/lib/screens/product_master_screen.dart @@ -159,37 +159,23 @@ class _ProductMasterScreenState extends State { 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 { ), ); } + + 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)), + ), + ], + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..9b1ac86 --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -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), + ), + ], + ), + ); + } +} diff --git a/lib/services/pdf_generator.dart b/lib/services/pdf_generator.dart index 3f05474..4479d7c 100644 --- a/lib/services/pdf_generator.dart +++ b/lib/services/pdf_generator.dart @@ -16,8 +16,7 @@ Future 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 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(