217 lines
No EOL
7.8 KiB
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,
|
|
});
|
|
} |