fix: keep invoice list visible when keyboard opens

This commit is contained in:
joe 2026-02-26 13:24:32 +09:00
parent 976ac6be20
commit 54135fa466

View file

@ -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,