177 lines
8.5 KiB
Dart
177 lines
8.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../models/invoice_models.dart';
|
|
import '../models/product_model.dart';
|
|
import '../services/product_repository.dart';
|
|
import '../widgets/keyboard_inset_wrapper.dart';
|
|
import '../widgets/screen_id_title.dart';
|
|
import 'product_master_screen.dart';
|
|
|
|
/// 商品マスターから項目を選択するためのモーダル(スタブ実装)
|
|
class ProductPickerModal extends StatefulWidget {
|
|
final Function(InvoiceItem) onItemSelected;
|
|
final ValueChanged<Product>? onProductSelected;
|
|
|
|
const ProductPickerModal({super.key, required this.onItemSelected, this.onProductSelected});
|
|
|
|
@override
|
|
State<ProductPickerModal> createState() => _ProductPickerModalState();
|
|
}
|
|
|
|
class _ProductPickerModalState extends State<ProductPickerModal> {
|
|
final ProductRepository _productRepo = ProductRepository();
|
|
final TextEditingController _searchController = TextEditingController();
|
|
List<Product> _products = [];
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_onSearch(""); // 初期表示
|
|
}
|
|
|
|
Future<void> _onSearch(String val) async {
|
|
setState(() => _isLoading = true);
|
|
final products = await _productRepo.searchProducts(val);
|
|
setState(() {
|
|
_products = products;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
child: Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppBar(
|
|
automaticallyImplyLeading: false,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: const ScreenAppBarTitle(screenId: 'P6', title: '商品・サービス選択'),
|
|
),
|
|
body: KeyboardInsetWrapper(
|
|
safeAreaTop: false,
|
|
basePadding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
|
extraBottom: 24,
|
|
child: Column(
|
|
children: [
|
|
TextField(
|
|
controller: _searchController,
|
|
autofocus: true,
|
|
decoration: InputDecoration(
|
|
hintText: "商品名・カテゴリ・バーコードで検索",
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
_onSearch("");
|
|
},
|
|
),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
|
),
|
|
onChanged: _onSearch,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Expanded(
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _products.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text("商品が見つかりません"),
|
|
TextButton(
|
|
onPressed: () async {
|
|
await Navigator.push(context, MaterialPageRoute(builder: (context) => const ProductMasterScreen()));
|
|
_onSearch(_searchController.text);
|
|
},
|
|
child: const Text("マスターに追加する"),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: _products.length,
|
|
itemBuilder: (context, index) {
|
|
final product = _products[index];
|
|
return ListTile(
|
|
leading: const Icon(Icons.inventory_2_outlined),
|
|
title: Text(product.name),
|
|
subtitle: Text("¥${product.defaultUnitPrice} (在庫: ${product.stockQuantity})"),
|
|
onTap: () {
|
|
widget.onProductSelected?.call(product);
|
|
widget.onItemSelected(
|
|
InvoiceItem(
|
|
productId: product.id,
|
|
description: product.name,
|
|
quantity: 1,
|
|
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);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|