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> { class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final CustomerRepository _customerRepo = CustomerRepository(); final CustomerRepository _customerRepo = CustomerRepository();
final TextEditingController _searchController = TextEditingController();
List<Customer> _customers = []; List<Customer> _customers = [];
List<Customer> _filtered = [];
bool _isLoading = true; bool _isLoading = true;
String _sortKey = 'name_asc';
@override @override
void initState() { void initState() {
@ -26,10 +29,26 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final customers = await _customerRepo.getAllCustomers(); final customers = await _customerRepo.getAllCustomers();
setState(() { setState(() {
_customers = customers; _customers = customers;
_applyFilter();
_isLoading = false; _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 { Future<void> _addOrEditCustomer({Customer? customer}) async {
final isEdit = customer != null; final isEdit = customer != null;
final displayNameController = TextEditingController(text: customer?.displayName ?? ""); final displayNameController = TextEditingController(text: customer?.displayName ?? "");
@ -116,49 +135,81 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), leading: const BackButton(),
title: const Text("顧客マスター管理"), title: const Text("顧客マスター"),
backgroundColor: Colors.blueGrey, 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 body: Column(
? const Center(child: CircularProgressIndicator()) children: [
: _customers.isEmpty Padding(
? const Center(child: Text("顧客が登録されていません")) padding: const EdgeInsets.all(12),
: ListView.builder( child: TextField(
itemCount: _customers.length, controller: _searchController,
itemBuilder: (context, index) { decoration: InputDecoration(
final c = _customers[index]; hintText: "名前で検索 (電話帳参照ボタンは詳細で)",
return ListTile( prefixIcon: const Icon(Icons.search),
title: Text(c.displayName), filled: true,
subtitle: Text("${c.formalName} ${c.title}"), fillColor: Colors.white,
trailing: Row( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
mainAxisSize: MainAxisSize.min, ),
children: [ onChanged: (_) => setState(_applyFilter),
IconButton(icon: const Icon(Icons.edit), onPressed: () => _addOrEditCustomer(customer: c)), ),
IconButton( ),
icon: const Icon(Icons.delete, color: Colors.red), Expanded(
onPressed: () async { child: _isLoading
final confirm = await showDialog<bool>( ? const Center(child: CircularProgressIndicator())
context: context, : _filtered.isEmpty
builder: (context) => AlertDialog( ? const Center(child: Text("顧客が登録されていません"))
title: const Text("削除確認"), : ListView.builder(
content: Text("${c.displayName}」を削除しますか?"), itemCount: _filtered.length,
actions: [ itemBuilder: (context, index) {
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")), final c = _filtered[index];
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))), return ListTile(
], leading: CircleAvatar(
), backgroundColor: c.isLocked ? Colors.grey.shade300 : Colors.indigo.shade100,
); child: Stack(
if (confirm == true) { children: [
await _customerRepo.deleteCustomer(c.id); const Align(alignment: Alignment.center, child: Icon(Icons.person, color: Colors.indigo)),
_loadCustomers(); 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( floatingActionButton: FloatingActionButton(
onPressed: () => _addOrEditCustomer(), onPressed: () => _addOrEditCustomer(),
child: const Icon(Icons.person_add), 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 '../services/customer_repository.dart';
import 'invoice_detail_page.dart'; import 'invoice_detail_page.dart';
import 'management_screen.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 'company_info_screen.dart';
import '../widgets/slide_to_unlock.dart'; import '../widgets/slide_to_unlock.dart';
import '../main.dart'; // InvoiceFlowScreen import '../main.dart'; // InvoiceFlowScreen
@ -98,6 +101,63 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
final dateFormatter = DateFormat('yyyy/MM/dd'); final dateFormatter = DateFormat('yyyy/MM/dd');
return Scaffold( 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( appBar: AppBar(
// leading removed // leading removed
title: GestureDetector( title: GestureDetector(

View file

@ -32,11 +32,11 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
Customer? _selectedCustomer; Customer? _selectedCustomer;
final List<InvoiceItem> _items = []; final List<InvoiceItem> _items = [];
double _taxRate = 0.10; double _taxRate = 0.10;
bool _includeTax = true; bool _includeTax = false;
CompanyInfo? _companyInfo; CompanyInfo? _companyInfo;
DocumentType _documentType = DocumentType.invoice; // DocumentType _documentType = DocumentType.invoice; //
DateTime _selectedDate = DateTime.now(); // : DateTime _selectedDate = DateTime.now(); // :
bool _isDraft = false; // : bool _isDraft = true; //
final TextEditingController _subjectController = TextEditingController(); // final TextEditingController _subjectController = TextEditingController(); //
bool _isSaving = false; // bool _isSaving = false; //
String _status = "取引先と商品を入力してください"; String _status = "取引先と商品を入力してください";
@ -74,7 +74,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
_isDraft = inv.isDraft; _isDraft = inv.isDraft;
if (inv.subject != null) _subjectController.text = inv.subject!; if (inv.subject != null) _subjectController.text = inv.subject!;
} else { } else {
_taxRate = companyInfo.defaultTaxRate; _taxRate = 0;
_includeTax = false;
_isDraft = true;
} }
}); });
} }
@ -202,15 +204,14 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fmt = NumberFormat("#,###"); final fmt = NumberFormat("#,###");
final themeColor = _isDraft ? Colors.blueGrey.shade800 : Colors.white; final themeColor = Colors.white;
final textColor = _isDraft ? Colors.white : Colors.black87; final textColor = Colors.black87;
return Scaffold( return Scaffold(
backgroundColor: themeColor, backgroundColor: themeColor,
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), leading: const BackButton(),
title: Text(_isDraft ? "伝票作成 (下書き)" : "販売アシスト1号 V1.5.06"), title: const Text("販売アシスト1号 V1.5.06"),
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
), ),
body: Stack( body: Stack(
children: [ children: [
@ -222,8 +223,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDraftToggle(),
const SizedBox(height: 16),
_buildDocumentTypeSection(), _buildDocumentTypeSection(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildDateSection(), _buildDateSection(),
@ -234,8 +233,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
const SizedBox(height: 20), const SizedBox(height: 20),
_buildItemsSection(fmt), _buildItemsSection(fmt),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildTaxSettings(),
const SizedBox(height: 20),
_buildSummarySection(fmt), _buildSummarySection(fmt),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildSignatureSection(), _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() { Widget _buildDateSection() {
final fmt = DateFormat('yyyy年MM月dd日'); final fmt = DateFormat('yyyy/MM/dd');
return Card( return GestureDetector(
elevation: 0, onTap: () async {
color: Colors.blueGrey.shade50, final picked = await showDatePicker(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), context: context,
child: ListTile( initialDate: _selectedDate,
leading: const Icon(Icons.calendar_today, color: Colors.blueGrey), firstDate: DateTime(2000),
title: Text("伝票日付: ${fmt.format(_selectedDate)}", style: const TextStyle(fontWeight: FontWeight.bold)), lastDate: DateTime(2100),
subtitle: const Text("タップして日付を変更"), );
trailing: const Icon(Icons.edit, size: 20), if (picked != null) {
onTap: () async { setState(() => _selectedDate = picked);
final picked = await showDatePicker( }
context: context, },
initialDate: _selectedDate, child: Container(
firstDate: DateTime(2020), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
lastDate: DateTime.now().add(const Duration(days: 365)), decoration: BoxDecoration(
); color: Colors.grey.shade100,
if (picked != null) { borderRadius: BorderRadius.circular(12),
setState(() => _selectedDate = picked); 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) { Widget _buildSummarySection(NumberFormat fmt) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.indigo.shade900, borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(color: Colors.indigo.shade900, borderRadius: BorderRadius.circular(12)),
child: Column( child: Column(
children: [ children: [
_buildSummaryRow(_includeTax ? "小計 (税抜)" : "小計", "${fmt.format(_subTotal)}", Colors.white70), _buildSummaryRow("小計", "${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),
],
const Divider(color: Colors.white24), 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: [ children: [
Expanded( Expanded(
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _showPreview, onPressed: _items.isEmpty ? null : _showPreview,
icon: const Icon(Icons.picture_as_pdf), // icon: const Icon(Icons.picture_as_pdf), //
label: const Text("PDFプレビュー"), // label: const Text("PDFプレビュー"), //
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@ -610,9 +530,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _saveInvoice(generatePdf: false), onPressed: () => _saveInvoice(generatePdf: false),
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
label: const Text("保存のみ"), label: const Text("保存"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey, backgroundColor: Colors.indigo,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16), 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) { Widget _buildSubjectSection(Color textColor) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View file

@ -159,37 +159,23 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final p = _filteredProducts[index]; final p = _filteredProducts[index];
return ListTile( return ListTile(
leading: const CircleAvatar(child: Icon(Icons.inventory_2)), leading: CircleAvatar(
title: Text(p.name), 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})"), subtitle: Text("${p.category ?? '未分類'} - ¥${p.defaultUnitPrice} (在庫: ${p.stockQuantity})"),
trailing: Row( onTap: () => _showDetailPane(p),
mainAxisSize: MainAxisSize.min, trailing: IconButton(
children: [ icon: const Icon(Icons.edit),
IconButton(icon: const Icon(Icons.edit), onPressed: () => _showEditDialog(product: p)), onPressed: p.isLocked ? null : () => _showEditDialog(product: p),
IconButton( tooltip: p.isLocked ? "ロック中" : "編集",
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)),
),
],
),
);
},
),
],
), ),
); );
}, },
@ -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 fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData); final ipaex = pw.Font.ttf(fontData);
final boldTtf = pw.Font.ttf(fontData);
final dateFormatter = DateFormat('yyyy年MM月dd日'); final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("#,###"); final amountFormatter = NumberFormat("#,###");
@ -40,7 +39,14 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
pw.MultiPage( pw.MultiPage(
pageFormat: PdfPageFormat.a4, pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32), 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) => [ build: (context) => [
// //
pw.Header( pw.Header(