fix: keep invoice list visible when keyboard opens
This commit is contained in:
parent
976ac6be20
commit
54135fa466
1 changed files with 148 additions and 134 deletions
|
|
@ -46,14 +46,11 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showInvoiceActions(Invoice invoice) async {
|
Future<void> _showInvoiceActions(Invoice invoice) async {
|
||||||
|
if (!_requireUnlock()) return;
|
||||||
if (invoice.isLocked) {
|
if (invoice.isLocked) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ロック中の伝票は操作できません")));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ロック中の伝票は操作できません")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_isUnlocked) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("操作するにはアンロックが必要です")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await showModalBottomSheet(
|
await showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
|
|
@ -93,41 +90,44 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
title: const Text("編集"),
|
title: const Text("編集"),
|
||||||
onTap: () async {
|
onTap: _isUnlocked
|
||||||
Navigator.pop(context);
|
? () async {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => InvoiceInputForm(
|
builder: (context) => InvoiceDetailPage(
|
||||||
existingInvoice: invoice,
|
invoice: invoice,
|
||||||
onInvoiceGenerated: (inv, path) {},
|
isUnlocked: _isUnlocked, // 状態を渡す
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_loadData();
|
_loadData();
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete, color: Colors.redAccent),
|
leading: const Icon(Icons.delete, color: Colors.redAccent),
|
||||||
title: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
title: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
||||||
onTap: () async {
|
onTap: _isUnlocked
|
||||||
Navigator.pop(context);
|
? () async {
|
||||||
final confirm = await showDialog<bool>(
|
Navigator.pop(context);
|
||||||
context: context,
|
final confirm = await showDialog<bool>(
|
||||||
builder: (context) => AlertDialog(
|
context: context,
|
||||||
title: const Text("伝票の削除"),
|
builder: (context) => AlertDialog(
|
||||||
content: Text("「${invoice.customerNameForDisplay}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
|
title: const Text("伝票の削除"),
|
||||||
actions: [
|
content: Text("「${invoice.customerNameForDisplay}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))),
|
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 _invoiceRepo.deleteInvoice(invoice.id);
|
if (confirm == true) {
|
||||||
_loadData();
|
await _invoiceRepo.deleteInvoice(invoice.id);
|
||||||
}
|
_loadData();
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -135,6 +135,12 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _requireUnlock() {
|
||||||
|
if (_isUnlocked) return true;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("スライドでロック解除してください")));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadVersion() async {
|
Future<void> _loadVersion() async {
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -193,65 +199,69 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final amountFormatter = NumberFormat("#,###");
|
final amountFormatter = NumberFormat("#,###");
|
||||||
final dateFormatter = DateFormat('yyyy/MM/dd');
|
final dateFormatter = DateFormat('yyyy/MM/dd');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: Drawer(
|
resizeToAvoidBottomInset: false,
|
||||||
child: ListView(
|
drawer: _isUnlocked
|
||||||
padding: EdgeInsets.zero,
|
? Drawer(
|
||||||
children: [
|
child: ListView(
|
||||||
DrawerHeader(
|
padding: EdgeInsets.zero,
|
||||||
decoration: BoxDecoration(color: Colors.indigo.shade700),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
const Text("メニュー", style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
|
DrawerHeader(
|
||||||
const SizedBox(height: 8),
|
decoration: BoxDecoration(color: Colors.indigo.shade700),
|
||||||
Text("v$_appVersion", style: const TextStyle(color: Colors.white70)),
|
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()));
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
ListTile(
|
: null,
|
||||||
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(
|
||||||
|
|
@ -317,20 +327,21 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: SafeArea(
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(16.0),
|
Padding(
|
||||||
child: SlideToUnlock(
|
padding: const EdgeInsets.all(16.0),
|
||||||
isLocked: !_isUnlocked,
|
child: SlideToUnlock(
|
||||||
onUnlocked: _toggleUnlock,
|
isLocked: !_isUnlocked,
|
||||||
text: "スライドでロック解除",
|
onUnlocked: _toggleUnlock,
|
||||||
|
text: "スライドでロック解除",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: _isLoading
|
||||||
child: _isLoading
|
? const Center(child: CircularProgressIndicator())
|
||||||
? const Center(child: CircularProgressIndicator())
|
: _filteredInvoices.isEmpty
|
||||||
: _filteredInvoices.isEmpty
|
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -342,7 +353,8 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.only(bottom: 100), // キーボードやFAB考慮
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
padding: const EdgeInsets.only(bottom: 120), // 固定: FAB+安全余白
|
||||||
itemCount: _filteredInvoices.length,
|
itemCount: _filteredInvoices.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final invoice = _filteredInvoices[index];
|
final invoice = _filteredInvoices[index];
|
||||||
|
|
@ -383,23 +395,20 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
),
|
),
|
||||||
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
height: 56,
|
height: 60,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text("¥${amountFormatter.format(invoice.totalAmount)}",
|
Text("¥${amountFormatter.format(invoice.totalAmount)}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
const SizedBox(height: 2),
|
|
||||||
if (invoice.isSynced)
|
if (invoice.isSynced)
|
||||||
const Icon(Icons.sync, size: 14, color: Colors.green)
|
const Icon(Icons.sync, size: 14, color: Colors.green)
|
||||||
else
|
else
|
||||||
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
|
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
|
||||||
const SizedBox(height: 4),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints.tightFor(width: 32, height: 28),
|
constraints: const BoxConstraints.tightFor(width: 32, height: 26),
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
tooltip: invoice.isLocked ? "ロック中" : (_isUnlocked ? "編集" : "アンロックして編集"),
|
tooltip: invoice.isLocked ? "ロック中" : (_isUnlocked ? "編集" : "アンロックして編集"),
|
||||||
onPressed: (invoice.isLocked || !_isUnlocked)
|
onPressed: (invoice.isLocked || !_isUnlocked)
|
||||||
|
|
@ -420,35 +429,40 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: _isUnlocked
|
||||||
await Navigator.push(
|
? () async {
|
||||||
context,
|
await Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (context) => InvoiceDetailPage(
|
MaterialPageRoute(
|
||||||
invoice: invoice,
|
builder: (context) => InvoiceDetailPage(
|
||||||
isUnlocked: _isUnlocked, // 状態を渡す
|
invoice: invoice,
|
||||||
),
|
isUnlocked: _isUnlocked, // 状態を渡す
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
_loadData();
|
);
|
||||||
},
|
_loadData();
|
||||||
onLongPress: () => _showInvoiceActions(invoice),
|
}
|
||||||
|
: () => _requireUnlock(),
|
||||||
|
onLongPress: _isUnlocked ? () => _showInvoiceActions(invoice) : () => _requireUnlock(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () async {
|
onPressed: _isUnlocked
|
||||||
await Navigator.push(
|
? () async {
|
||||||
context,
|
await Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (context) => InvoiceFlowScreen(onComplete: _loadData),
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) => InvoiceFlowScreen(onComplete: _loadData),
|
||||||
);
|
),
|
||||||
_loadData();
|
);
|
||||||
},
|
_loadData();
|
||||||
|
}
|
||||||
|
: _requireUnlock,
|
||||||
label: const Text("新規伝票作成"),
|
label: const Text("新規伝票作成"),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue