分割の後片付け終了
This commit is contained in:
parent
7baba0091b
commit
39759be02a
7 changed files with 499 additions and 326 deletions
|
|
@ -299,15 +299,17 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setDialogState) {
|
builder: (context, setDialogState) {
|
||||||
return AlertDialog(
|
final inset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
return MediaQuery.removeViewInsets(
|
||||||
|
removeBottom: true,
|
||||||
|
context: context,
|
||||||
|
child: AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||||
title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"),
|
title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"),
|
||||||
content: KeyboardInsetWrapper(
|
content: SingleChildScrollView(
|
||||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
|
||||||
extraBottom: 32,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
padding: EdgeInsets.only(bottom: inset + 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -396,7 +398,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -423,6 +425,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
child: const Text("保存"),
|
child: const Text("保存"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -663,6 +666,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: Text(widget.selectionMode ? "C2:顧客選択" : "C1:顧客一覧"),
|
title: Text(widget.selectionMode ? "C2:顧客選択" : "C1:顧客一覧"),
|
||||||
|
|
@ -712,12 +716,14 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: KeyboardInsetWrapper(
|
body: Padding(
|
||||||
basePadding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
|
padding: const EdgeInsets.only(top: 8, bottom: 8),
|
||||||
extraBottom: 40,
|
child: CustomScrollView(
|
||||||
child: Column(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: [
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
Padding(
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
|
|
@ -731,8 +737,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
onChanged: (_) => setState(_applyFilter),
|
onChanged: (_) => setState(_applyFilter),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (!widget.selectionMode)
|
if (!widget.selectionMode)
|
||||||
Padding(
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
title: const Text('株式会社/有限会社などの接頭辞を無視してソート'),
|
title: const Text('株式会社/有限会社などの接頭辞を無視してソート'),
|
||||||
|
|
@ -743,13 +751,21 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: _isLoading
|
if (_isLoading)
|
||||||
? const Center(child: CircularProgressIndicator())
|
const SliverFillRemaining(
|
||||||
: _filtered.isEmpty
|
hasScrollBody: false,
|
||||||
? const Center(child: Text("顧客が登録されていません"))
|
child: Center(child: CircularProgressIndicator()),
|
||||||
: ListView.builder(
|
)
|
||||||
padding: const EdgeInsets.only(bottom: 120, top: 4),
|
else if (_filtered.isEmpty)
|
||||||
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(child: Text("顧客が登録されていません")),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 80, top: 4),
|
||||||
|
sliver: SliverList.builder(
|
||||||
itemCount: _filtered.length,
|
itemCount: _filtered.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final c = _filtered[index];
|
final c = _filtered[index];
|
||||||
|
|
@ -782,12 +798,16 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return FloatingActionButton.extended(
|
||||||
onPressed: _showAddMenu,
|
onPressed: _showAddMenu,
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(widget.selectionMode ? "選択" : "追加"),
|
label: Text(widget.selectionMode ? "選択" : "追加"),
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,11 +222,13 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
child: KeyboardInsetWrapper(
|
child: KeyboardInsetWrapper(
|
||||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
|
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||||
extraBottom: 24,
|
extraBottom: 32,
|
||||||
child: Column(
|
child: CustomScrollView(
|
||||||
children: [
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
Padding(
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -262,15 +264,22 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
Expanded(
|
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
child: _isLoading
|
if (_isLoading)
|
||||||
? const Center(child: CircularProgressIndicator())
|
const SliverFillRemaining(
|
||||||
: _filteredCustomers.isEmpty
|
hasScrollBody: false,
|
||||||
? const Center(child: Text("該当する顧客がいません"))
|
child: Center(child: CircularProgressIndicator()),
|
||||||
: ListView.builder(
|
)
|
||||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
else if (_filteredCustomers.isEmpty)
|
||||||
padding: const EdgeInsets.only(bottom: 80),
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(child: Text("該当する顧客がいません")),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 120),
|
||||||
|
sliver: SliverList.builder(
|
||||||
itemCount: _filteredCustomers.length,
|
itemCount: _filteredCustomers.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final customer = _filteredCustomers[index];
|
final customer = _filteredCustomers[index];
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import 'invoice_input_screen.dart';
|
||||||
import 'settings_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 用
|
// InvoiceFlowScreen import removed; using inline type picker
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import '../widgets/invoice_pdf_preview_page.dart';
|
import '../widgets/invoice_pdf_preview_page.dart';
|
||||||
import 'invoice_history/invoice_history_list.dart';
|
import 'invoice_history/invoice_history_list.dart';
|
||||||
|
|
@ -375,15 +375,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: _isUnlocked
|
onPressed: _isUnlocked
|
||||||
? () async {
|
? () => _showCreateTypeMenu()
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => InvoiceFlowScreen(onComplete: _loadData),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_loadData();
|
|
||||||
}
|
|
||||||
: _requireUnlock,
|
: _requireUnlock,
|
||||||
label: const Text("新規伝票作成"),
|
label: const Text("新規伝票作成"),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
|
|
@ -392,4 +384,51 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showCreateTypeMenu() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.insert_drive_file_outlined),
|
||||||
|
title: const Text('下書き: 見積書', style: TextStyle(fontSize: 24)),
|
||||||
|
onTap: () => _startNew(DocumentType.estimation),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.local_shipping_outlined),
|
||||||
|
title: const Text('下書き: 納品書', style: TextStyle(fontSize: 24)),
|
||||||
|
onTap: () => _startNew(DocumentType.delivery),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.request_quote_outlined),
|
||||||
|
title: const Text('下書き: 請求書', style: TextStyle(fontSize: 24)),
|
||||||
|
onTap: () => _startNew(DocumentType.invoice),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.receipt_long_outlined),
|
||||||
|
title: const Text('下書き: 領収書', style: TextStyle(fontSize: 24)),
|
||||||
|
onTap: () => _startNew(DocumentType.receipt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startNew(DocumentType type) async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => InvoiceInputForm(
|
||||||
|
onInvoiceGenerated: (inv, path) {},
|
||||||
|
initialDocumentType: type,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_loadData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,19 @@ import '../widgets/invoice_pdf_preview_page.dart';
|
||||||
import 'invoice_detail_page.dart';
|
import 'invoice_detail_page.dart';
|
||||||
import '../services/gps_service.dart';
|
import '../services/gps_service.dart';
|
||||||
import 'customer_master_screen.dart';
|
import 'customer_master_screen.dart';
|
||||||
import 'product_picker_modal.dart';
|
import 'product_master_screen.dart';
|
||||||
import '../widgets/keyboard_inset_wrapper.dart';
|
import '../models/product_model.dart';
|
||||||
|
|
||||||
class InvoiceInputForm extends StatefulWidget {
|
class InvoiceInputForm extends StatefulWidget {
|
||||||
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
|
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
|
||||||
final Invoice? existingInvoice; // 追加: 編集時の既存伝票
|
final Invoice? existingInvoice; // 追加: 編集時の既存伝票
|
||||||
|
final DocumentType initialDocumentType;
|
||||||
|
|
||||||
const InvoiceInputForm({
|
const InvoiceInputForm({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onInvoiceGenerated,
|
required this.onInvoiceGenerated,
|
||||||
this.existingInvoice, // 追加
|
this.existingInvoice, // 追加
|
||||||
|
this.initialDocumentType = DocumentType.invoice,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -72,21 +74,26 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
_taxRate = 0;
|
_taxRate = 0;
|
||||||
_includeTax = false;
|
_includeTax = false;
|
||||||
_isDraft = true;
|
_isDraft = true;
|
||||||
|
_documentType = widget.initialDocumentType;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addItem() {
|
void _addItem() {
|
||||||
showModalBottomSheet(
|
Navigator.push<Product>(
|
||||||
context: context,
|
context,
|
||||||
isScrollControlled: true,
|
MaterialPageRoute(builder: (_) => const ProductMasterScreen(selectionMode: true)),
|
||||||
builder: (context) => ProductPickerModal(
|
).then((product) {
|
||||||
onItemSelected: (item) {
|
if (product == null) return;
|
||||||
setState(() => _items.add(item));
|
setState(() {
|
||||||
Navigator.pop(context);
|
_items.add(InvoiceItem(
|
||||||
},
|
productId: product.id,
|
||||||
),
|
description: product.name,
|
||||||
);
|
quantity: 1,
|
||||||
|
unitPrice: product.defaultUnitPrice,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
int get _subTotal => _items.fold(0, (sum, item) => sum + (item.unitPrice * item.quantity));
|
int get _subTotal => _items.fold(0, (sum, item) => sum + (item.unitPrice * item.quantity));
|
||||||
|
|
@ -215,19 +222,12 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
KeyboardInsetWrapper(
|
Column(
|
||||||
basePadding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
|
||||||
extraBottom: 24,
|
|
||||||
child: InteractiveViewer(
|
|
||||||
panEnabled: false,
|
|
||||||
minScale: 0.8,
|
|
||||||
maxScale: 2.5,
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 160),
|
padding: EdgeInsets.fromLTRB(16, 16, 16, MediaQuery.of(context).viewInsets.bottom + 140),
|
||||||
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -250,8 +250,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
_buildBottomActionBar(),
|
_buildBottomActionBar(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_isSaving)
|
if (_isSaving)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
|
|
@ -404,17 +402,11 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.search, size: 18),
|
icon: const Icon(Icons.search, size: 18),
|
||||||
label: const Text("マスター参照"),
|
label: const Text("マスター参照"),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
showModalBottomSheet(
|
Navigator.pop(context); // close edit dialog before jumping
|
||||||
context: context,
|
await Navigator.push(
|
||||||
isScrollControlled: true,
|
this.context,
|
||||||
builder: (context) => ProductPickerModal(
|
MaterialPageRoute(builder: (_) => const ProductMasterScreen()),
|
||||||
onItemSelected: (selected) {
|
|
||||||
descCtrl.text = selected.description;
|
|
||||||
priceCtrl.text = selected.unitPrice.toString();
|
|
||||||
Navigator.pop(context); // close picker
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import 'package:uuid/uuid.dart';
|
||||||
import '../models/product_model.dart';
|
import '../models/product_model.dart';
|
||||||
import '../services/product_repository.dart';
|
import '../services/product_repository.dart';
|
||||||
import 'barcode_scanner_screen.dart';
|
import 'barcode_scanner_screen.dart';
|
||||||
import '../widgets/keyboard_inset_wrapper.dart';
|
|
||||||
|
|
||||||
class ProductMasterScreen extends StatefulWidget {
|
class ProductMasterScreen extends StatefulWidget {
|
||||||
const ProductMasterScreen({super.key});
|
final bool selectionMode;
|
||||||
|
|
||||||
|
const ProductMasterScreen({super.key, this.selectionMode = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProductMasterScreen> createState() => _ProductMasterScreenState();
|
State<ProductMasterScreen> createState() => _ProductMasterScreenState();
|
||||||
|
|
@ -59,12 +60,17 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
final result = await showDialog<Product>(
|
final result = await showDialog<Product>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setDialogState) => AlertDialog(
|
builder: (context, setDialogState) {
|
||||||
|
final inset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
return MediaQuery.removeViewInsets(
|
||||||
|
removeBottom: true,
|
||||||
|
context: context,
|
||||||
|
child: AlertDialog(
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
title: Text(product == null ? "商品追加" : "商品編集"),
|
title: Text(product == null ? "商品追加" : "商品編集"),
|
||||||
content: KeyboardInsetWrapper(
|
content: SingleChildScrollView(
|
||||||
basePadding: EdgeInsets.zero,
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
extraBottom: 16,
|
padding: EdgeInsets.only(bottom: inset + 12),
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -95,7 +101,6 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|
@ -118,6 +123,8 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -131,6 +138,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: const Text("P1:商品マスター"),
|
title: const Text("P1:商品マスター"),
|
||||||
|
|
@ -157,15 +165,15 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: KeyboardInsetWrapper(
|
body: Padding(
|
||||||
basePadding: EdgeInsets.zero,
|
padding: const EdgeInsets.only(top: 8, bottom: 8),
|
||||||
extraBottom: 72,
|
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _filteredProducts.isEmpty
|
: _filteredProducts.isEmpty
|
||||||
? const Center(child: Text("商品が見つかりません"))
|
? const Center(child: Text("商品が見つかりません"))
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.only(bottom: 120, top: 8),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(bottom: 80, top: 8),
|
||||||
itemCount: _filteredProducts.length,
|
itemCount: _filteredProducts.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final p = _filteredProducts[index];
|
final p = _filteredProducts[index];
|
||||||
|
|
@ -182,8 +190,59 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
||||||
),
|
),
|
||||||
title: Text(p.name, style: TextStyle(fontWeight: FontWeight.bold, color: p.isLocked ? Colors.grey : Colors.black87)),
|
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})"),
|
||||||
onTap: () => _showDetailPane(p),
|
onTap: () {
|
||||||
trailing: IconButton(
|
if (widget.selectionMode) {
|
||||||
|
Navigator.pop(context, p);
|
||||||
|
} else {
|
||||||
|
_showDetailPane(p);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () async {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.edit),
|
||||||
|
title: const Text("編集"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_showEditDialog(product: p);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!p.isLocked)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||||||
|
title: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text("削除の確認"),
|
||||||
|
content: Text("${p.name} を削除しますか?"),
|
||||||
|
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 (confirmed == true) {
|
||||||
|
await _productRepo.deleteProduct(p.id);
|
||||||
|
if (mounted) _loadProducts();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
trailing: widget.selectionMode
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: p.isLocked ? null : () => _showEditDialog(product: p),
|
onPressed: p.isLocked ? null : () => _showEditDialog(product: p),
|
||||||
tooltip: p.isLocked ? "ロック中" : "編集",
|
tooltip: p.isLocked ? "ロック中" : "編集",
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,15 @@ class _ProductPickerModalState extends State<ProductPickerModal> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.fromLTRB(8, 8, 16, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
const Text("商品・サービス選択", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
const Text("商品・サービス選択", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -101,14 +104,63 @@ class _ProductPickerModalState extends State<ProductPickerModal> {
|
||||||
leading: const Icon(Icons.inventory_2_outlined),
|
leading: const Icon(Icons.inventory_2_outlined),
|
||||||
title: Text(product.name),
|
title: Text(product.name),
|
||||||
subtitle: Text("¥${product.defaultUnitPrice} (在庫: ${product.stockQuantity})"),
|
subtitle: Text("¥${product.defaultUnitPrice} (在庫: ${product.stockQuantity})"),
|
||||||
onTap: () => widget.onItemSelected(
|
onTap: () {
|
||||||
|
widget.onItemSelected(
|
||||||
InvoiceItem(
|
InvoiceItem(
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
description: product.name,
|
description: product.name,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unitPrice: product.defaultUnitPrice,
|
unitPrice: product.defaultUnitPrice,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
onLongPress: () async {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.edit),
|
||||||
|
title: const Text("編集"),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const ProductMasterScreen()),
|
||||||
|
);
|
||||||
|
_onSearch(_searchController.text);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||||||
|
title: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text("削除の確認"),
|
||||||
|
content: Text("${product.name} を削除しますか?"),
|
||||||
|
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 (confirmed == true) {
|
||||||
|
await _productRepo.deleteProduct(product.id);
|
||||||
|
if (mounted) _onSearch(_searchController.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../widgets/keyboard_inset_wrapper.dart';
|
|
||||||
import 'company_info_screen.dart';
|
import 'company_info_screen.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
|
|
@ -227,6 +226,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
final listBottomPadding = 24 + bottomInset;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -238,11 +239,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: KeyboardInsetWrapper(
|
body: Padding(
|
||||||
basePadding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||||
extraBottom: 40,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: EdgeInsets.only(bottom: listBottomPadding),
|
||||||
children: [
|
children: [
|
||||||
_section(
|
_section(
|
||||||
title: '自社情報',
|
title: '自社情報',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue