分割に失敗、修復中
This commit is contained in:
parent
504a5a60cc
commit
c01a0b6775
11 changed files with 966 additions and 764 deletions
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
import '../models/company_model.dart';
|
||||
import '../services/company_repository.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class CompanyInfoScreen extends StatefulWidget {
|
||||
const CompanyInfoScreen({Key? key}) : super(key: key);
|
||||
|
|
@ -76,8 +77,11 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
|
|||
IconButton(icon: const Icon(Icons.check), onPressed: _save),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
body: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.all(16),
|
||||
extraBottom: 32,
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -142,6 +146,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:convert';
|
||||
import '../models/customer_model.dart';
|
||||
|
|
@ -315,9 +316,14 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
final result = await showDialog<Customer>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
builder: (context, setDialogState) {
|
||||
return AlertDialog(
|
||||
title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"),
|
||||
content: SingleChildScrollView(
|
||||
content: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
||||
extraBottom: 20,
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -423,6 +429,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
TextButton(
|
||||
|
|
@ -449,7 +456,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
child: const Text("保存"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -741,7 +749,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
body: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
|
||||
extraBottom: 40,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
|
|
@ -757,7 +768,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
onChanged: (_) => setState(_applyFilter),
|
||||
),
|
||||
),
|
||||
// Kana index temporarily disabled
|
||||
if (!widget.selectionMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
|
|
@ -776,6 +786,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
: _filtered.isEmpty
|
||||
? const Center(child: Text("顧客が登録されていません"))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 120, top: 4),
|
||||
itemCount: _filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final c = _filtered[index];
|
||||
|
|
@ -807,10 +818,11 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _showAddMenu,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('顧客を追加'),
|
||||
label: Text(widget.selectionMode ? "選択" : "追加"),
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
|
|
@ -963,7 +975,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showContactUpdateDialog(c);
|
||||
_showContactUpdateSheet(c);
|
||||
},
|
||||
icon: const Icon(Icons.contact_mail),
|
||||
label: const Text("連絡先を更新"),
|
||||
|
|
@ -1006,4 +1018,38 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContactUpdateSheet(Customer c) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
||||
extraBottom: 16,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.contact_mail),
|
||||
title: const Text('連絡先を更新'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showContactUpdateSheet(c);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.contact_phone),
|
||||
title: const Text('電話帳から取り込む'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showPhonebookImport();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
|||
import 'package:uuid/uuid.dart';
|
||||
import '../models/customer_model.dart';
|
||||
import '../services/customer_repository.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
/// 顧客マスターからの選択、登録、編集、削除を行うモーダル
|
||||
class CustomerPickerModal extends StatefulWidget {
|
||||
|
|
@ -203,6 +204,9 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
|
||||
extraBottom: 24,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
|
|
@ -241,13 +245,15 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredCustomers.isEmpty
|
||||
? const Center(child: Text("該当する顧客がいません"))
|
||||
: ListView.builder(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
itemCount: _filteredCustomers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final customer = _filteredCustomers[index];
|
||||
|
|
@ -279,6 +285,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import '../services/customer_repository.dart';
|
|||
import '../services/company_repository.dart';
|
||||
import 'product_picker_modal.dart';
|
||||
import '../models/company_model.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class InvoiceDetailPage extends StatefulWidget {
|
||||
final Invoice invoice;
|
||||
|
|
@ -158,6 +159,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
|
||||
return Scaffold(
|
||||
backgroundColor: themeColor,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(), // 常に表示
|
||||
title: Row(
|
||||
|
|
@ -245,8 +247,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
]
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
body: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.all(16.0),
|
||||
extraBottom: 48,
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -315,11 +320,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection(Color textColor) {
|
||||
final dateFormatter = DateFormat('yyyy年MM月dd日');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
|
|||
110
lib/screens/invoice_history/invoice_history_item.dart
Normal file
110
lib/screens/invoice_history/invoice_history_item.dart
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/invoice_models.dart';
|
||||
|
||||
class InvoiceHistoryItem extends StatelessWidget {
|
||||
final Invoice invoice;
|
||||
final bool isUnlocked;
|
||||
final NumberFormat amountFormatter;
|
||||
final DateFormat dateFormatter;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
const InvoiceHistoryItem({
|
||||
Key? key,
|
||||
required this.invoice,
|
||||
required this.isUnlocked,
|
||||
required this.amountFormatter,
|
||||
required this.dateFormatter,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onEdit,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
tileColor: invoice.isDraft ? Colors.orange.shade50 : null,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: invoice.isDraft
|
||||
? Colors.orange.shade100
|
||||
: (isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
|
||||
color: invoice.isDraft
|
||||
? Colors.orange
|
||||
: (isUnlocked ? Colors.indigo : Colors.grey),
|
||||
),
|
||||
),
|
||||
if (invoice.isLocked)
|
||||
const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Icon(Icons.lock, size: 14, color: Colors.redAccent),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
invoice.customerNameForDisplay,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: invoice.isLocked ? Colors.grey : Colors.black87,
|
||||
),
|
||||
),
|
||||
if (invoice.subject?.isNotEmpty ?? false)
|
||||
Text(
|
||||
invoice.subject!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.indigo.shade700,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"¥${amountFormatter.format(invoice.totalAmount)}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
if (invoice.isSynced)
|
||||
const Icon(Icons.sync, size: 14, color: Colors.green)
|
||||
else
|
||||
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(width: 28, height: 24),
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
tooltip: invoice.isLocked
|
||||
? "ロック中"
|
||||
: (isUnlocked ? "編集" : "アンロックして編集"),
|
||||
onPressed: (invoice.isLocked || !isUnlocked)
|
||||
? null
|
||||
: onEdit,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
60
lib/screens/invoice_history/invoice_history_list.dart
Normal file
60
lib/screens/invoice_history/invoice_history_list.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/invoice_models.dart';
|
||||
import 'invoice_history_item.dart';
|
||||
|
||||
class InvoiceHistoryList extends StatelessWidget {
|
||||
final List<Invoice> invoices;
|
||||
final bool isUnlocked;
|
||||
final NumberFormat amountFormatter;
|
||||
final DateFormat dateFormatter;
|
||||
final void Function(Invoice) onTap;
|
||||
final void Function(Invoice) onLongPress;
|
||||
final void Function(Invoice) onEdit;
|
||||
|
||||
const InvoiceHistoryList({
|
||||
Key? key,
|
||||
required this.invoices,
|
||||
required this.isUnlocked,
|
||||
required this.amountFormatter,
|
||||
required this.dateFormatter,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
required this.onEdit,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (invoices.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.folder_open, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text("保存された伝票がありません"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: const EdgeInsets.only(bottom: 120), // FAB分の固定余白
|
||||
itemCount: invoices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invoice = invoices[index];
|
||||
return InvoiceHistoryItem(
|
||||
invoice: invoice,
|
||||
isUnlocked: isUnlocked,
|
||||
amountFormatter: amountFormatter,
|
||||
dateFormatter: dateFormatter,
|
||||
onTap: () => onTap(invoice),
|
||||
onLongPress: () => onLongPress(invoice),
|
||||
onEdit: () => onEdit(invoice),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import '../main.dart'; // InvoiceFlowScreen 用
|
|||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import '../widgets/invoice_pdf_preview_page.dart';
|
||||
import 'invoice_history/invoice_history_list.dart';
|
||||
|
||||
class InvoiceHistoryScreen extends StatefulWidget {
|
||||
const InvoiceHistoryScreen({Key? key}) : super(key: key);
|
||||
|
|
@ -341,79 +342,26 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredInvoices.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.folder_open, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
Text(_searchQuery.isEmpty ? "保存された伝票がありません" : "該当する伝票が見つかりません"),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
padding: const EdgeInsets.only(bottom: 120), // 固定: FAB+安全余白
|
||||
itemCount: _filteredInvoices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invoice = _filteredInvoices[index];
|
||||
return ListTile(
|
||||
tileColor: invoice.isDraft ? Colors.orange.shade50 : null, // 下書きは背景色を変更
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: invoice.isDraft
|
||||
? Colors.orange.shade100
|
||||
: (_isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
invoice.isDraft ? Icons.edit_note : Icons.description_outlined,
|
||||
color: invoice.isDraft
|
||||
? Colors.orange
|
||||
: (_isUnlocked ? Colors.indigo : Colors.grey),
|
||||
: InvoiceHistoryList(
|
||||
invoices: _filteredInvoices,
|
||||
isUnlocked: _isUnlocked,
|
||||
amountFormatter: amountFormatter,
|
||||
dateFormatter: dateFormatter,
|
||||
onTap: (invoice) async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InvoiceDetailPage(
|
||||
invoice: invoice,
|
||||
isUnlocked: _isUnlocked,
|
||||
),
|
||||
),
|
||||
if (invoice.isLocked)
|
||||
const Align(alignment: Alignment.bottomRight, child: Icon(Icons.lock, size: 14, color: Colors.redAccent)),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(invoice.customerNameForDisplay, style: TextStyle(fontWeight: FontWeight.bold, color: invoice.isLocked ? Colors.grey : Colors.black87)),
|
||||
if (invoice.subject?.isNotEmpty ?? false)
|
||||
Text(
|
||||
invoice.subject!,
|
||||
style: TextStyle(fontSize: 13, color: Colors.indigo.shade700, fontWeight: FontWeight.normal),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
||||
trailing: SizedBox(
|
||||
height: 60,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text("¥${amountFormatter.format(invoice.totalAmount)}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
if (invoice.isSynced)
|
||||
const Icon(Icons.sync, size: 14, color: Colors.green)
|
||||
else
|
||||
const Icon(Icons.sync_disabled, size: 14, color: Colors.orange),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(width: 32, height: 26),
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
tooltip: invoice.isLocked ? "ロック中" : (_isUnlocked ? "編集" : "アンロックして編集"),
|
||||
onPressed: (invoice.isLocked || !_isUnlocked)
|
||||
? null
|
||||
: () async {
|
||||
);
|
||||
_loadData();
|
||||
},
|
||||
onLongPress: (invoice) => _isUnlocked ? _showInvoiceActions(invoice) : _requireUnlock(),
|
||||
onEdit: (invoice) async {
|
||||
if (invoice.isLocked || !_isUnlocked) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -426,27 +374,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
_loadData();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: _isUnlocked
|
||||
? () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InvoiceDetailPage(
|
||||
invoice: invoice,
|
||||
isUnlocked: _isUnlocked, // 状態を渡す
|
||||
),
|
||||
),
|
||||
);
|
||||
_loadData();
|
||||
}
|
||||
: () => _requireUnlock(),
|
||||
onLongPress: _isUnlocked ? () => _showInvoiceActions(invoice) : () => _requireUnlock(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'customer_master_screen.dart';
|
|||
import 'product_picker_modal.dart';
|
||||
import '../models/company_model.dart';
|
||||
import '../services/company_repository.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class InvoiceInputForm extends StatefulWidget {
|
||||
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
|
||||
|
|
@ -226,7 +227,9 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
extraBottom: 24,
|
||||
child: InteractiveViewer(
|
||||
panEnabled: false,
|
||||
minScale: 0.8,
|
||||
|
|
@ -236,7 +239,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 140),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 160),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart';
|
|||
import '../models/product_model.dart';
|
||||
import '../services/product_repository.dart';
|
||||
import 'barcode_scanner_screen.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class ProductMasterScreen extends StatefulWidget {
|
||||
const ProductMasterScreen({Key? key}) : super(key: key);
|
||||
|
|
@ -59,7 +60,10 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(product == null ? "商品追加" : "商品編集"),
|
||||
content: SingleChildScrollView(
|
||||
content: KeyboardInsetWrapper(
|
||||
basePadding: EdgeInsets.zero,
|
||||
extraBottom: 16,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -90,6 +94,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
|
|
@ -150,11 +155,15 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
body: KeyboardInsetWrapper(
|
||||
basePadding: EdgeInsets.zero,
|
||||
extraBottom: 72,
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredProducts.isEmpty
|
||||
? const Center(child: Text("商品が見つかりません"))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 120, top: 8),
|
||||
itemCount: _filteredProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final p = _filteredProducts[index];
|
||||
|
|
@ -180,6 +189,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showEditDialog(),
|
||||
child: const Icon(Icons.add),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
import 'company_info_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
|
|
@ -226,7 +227,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
|
|
@ -235,16 +235,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () => _showSnackbar('設定はテンプレ実装です。実際の保存は未実装'),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
padding: EdgeInsets.only(bottom: bottomInset),
|
||||
body: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
extraBottom: 40,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
children: [
|
||||
_section(
|
||||
|
|
@ -452,7 +449,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
33
lib/widgets/keyboard_inset_wrapper.dart
Normal file
33
lib/widgets/keyboard_inset_wrapper.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Wraps content with SafeArea and animated bottom padding based on keyboard.
|
||||
/// Use this to keep forms scrollable without Scaffold resizing.
|
||||
class KeyboardInsetWrapper extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsets basePadding;
|
||||
final double extraBottom;
|
||||
final Duration duration;
|
||||
final Curve curve;
|
||||
|
||||
const KeyboardInsetWrapper({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.basePadding = EdgeInsets.zero,
|
||||
this.extraBottom = 0,
|
||||
this.duration = const Duration(milliseconds: 180),
|
||||
this.curve = Curves.easeOut,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
return SafeArea(
|
||||
child: AnimatedPadding(
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
padding: basePadding.add(EdgeInsets.only(bottom: bottomInset + extraBottom)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue