h-1.flutter.4/lib/screens/sales_screen.dart

217 lines
No EOL
7.8 KiB
Dart

// Version: 1.10 - 売上入力画面(簡易実装)
import 'package:flutter/material.dart';
import '../services/database_helper.dart';
import '../models/product.dart';
import '../models/customer.dart';
class SalesScreen extends StatefulWidget {
const SalesScreen({super.key});
@override
State<SalesScreen> createState() => _SalesScreenState();
}
class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
List<Product> products = <Product>[];
List<_SaleItem> saleItems = <_SaleItem>[];
double totalAmount = 0.0;
Customer? selectedCustomer;
Future<void> loadProducts() async {
try {
final ps = await DatabaseHelper.instance.getProducts();
if (mounted) setState(() => products = ps ?? const <Product>[]);
} catch (e) {}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadProducts());
}
Future<void> searchProduct(String keyword) async {
if (!mounted || keyword.isEmpty) return;
final keywordLower = keyword.toLowerCase();
final matchedProducts = products.where((p) =>
(p.name?.toLowerCase() ?? '').contains(keywordLower) ||
(p.productCode ?? '').contains(keyword)).toList();
setState(() {
for (final product in matchedProducts) {
final existingItemIndex = saleItems.indexWhere((item) => item.productId == product.id);
if (existingItemIndex == -1 || saleItems[existingItemIndex].quantity < 1) {
saleItems.add(_SaleItem(
productId: product.id ?? 0,
productName: product.name ?? '',
productCode: product.productCode ?? '',
unitPrice: product.unitPrice ?? 0.0,
quantity: 1,
totalAmount: (product.unitPrice ?? 0.0),
));
} else {
saleItems[existingItemIndex].quantity += 1;
saleItems[existingItemIndex].totalAmount =
saleItems[existingItemIndex].unitPrice * saleItems[existingItemIndex].quantity;
}
}
calculateTotal();
});
}
void removeItem(int index) {
if (index >= 0 && index < saleItems.length) {
saleItems.removeAt(index);
calculateTotal();
}
}
void calculateTotal() {
final items = saleItems.map((item) => item.totalAmount).toList();
setState(() => totalAmount = items.fold(0, (sum, val) => sum + val));
}
Future<void> saveSale() async {
if (saleItems.isEmpty || !mounted) return;
try {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('売上データ保存'),
content: Text('入力した商品情報を販売アシストに保存します。'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () async {
await DatabaseHelper.instance.insertSales({
'id': DateTime.now().millisecondsSinceEpoch,
'customer_id': selectedCustomer?.id ?? 1,
'sale_date': DateTime.now().toIso8601String(),
'total_amount': (totalAmount * 1.1).round(),
'tax_rate': 8,
});
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('売上データ保存完了'), duration: Duration(seconds: 2)),
);
},
child: const Text('保存'),
),
],
),
);
} catch (e) {
WidgetsBinding.instance.addPostFrameCallback((_) => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red),
));
}
}
void showInvoiceDialog() {
if (saleItems.isEmpty || !mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('売上伝票'),
content: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('得意先:${selectedCustomer?.name ?? '未指定'}', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('商品数:${saleItems.length}'),
const SizedBox(height: 4),
Text('合計:¥${totalAmount.toStringAsFixed(0)}', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.teal)),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(child: const Text('閉じる'), onPressed: () => Navigator.pop(context),),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('売上入力'), actions: [
IconButton(icon: const Icon(Icons.save), onPressed: saveSale,),
IconButton(icon: const Icon(Icons.print, color: Colors.blue), onPressed: () => showInvoiceDialog(),),
]),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Text('レジモード', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(children: <Widget>[Text('合計'), const Icon(Icons.payments, size: 32)]),
const SizedBox(height: 4),
Text('¥${totalAmount.toStringAsFixed(0)}', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.teal)),
],),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
decoration: InputDecoration(labelText: '商品検索', hintText: 'JAN コードまたは商品名を入力して選択', prefixIcon: const Icon(Icons.search)),
onChanged: searchProduct,
),
),
Expanded(
child: saleItems.isEmpty
? const Center(child: Text('商品を登録'))
: ListView.separated(
itemCount: saleItems.length,
itemBuilder: (context, index) {
final item = saleItems[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Card(
child: ListTile(
leading: CircleAvatar(child: Icon(Icons.store)),
title: Text(item.productName ?? ''),
subtitle: Text('コード:${item.productCode} / ¥${item.totalAmount.toStringAsFixed(0)}'),
trailing: IconButton(icon: const Icon(Icons.remove_circle_outline), onPressed: () => removeItem(index),),
),
),
);
},
separatorBuilder: (_, __) => const Divider(),
),
),
],
),
);
}
}
class _SaleItem {
final int productId;
final String productName;
final String productCode;
final double unitPrice;
int quantity;
double totalAmount;
_SaleItem({
required this.productId,
required this.productName,
required this.productCode,
required this.unitPrice,
required this.quantity,
required this.totalAmount,
});
}