feat: add settings menu and explorer-style master UIs
This commit is contained in:
parent
145f0d7cad
commit
60fc0b46ac
6 changed files with 484 additions and 236 deletions
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
lib/screens/settings_screen.dart
Normal file
103
lib/screens/settings_screen.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue