h-1.flutter.4/lib/screens/sales_screen.dart
joe 9cec464868 feat: 各画面の AppBar に画面 ID を追加
- estimate_screen.dart: /S1. 見積入力
- invoice_screen.dart: /S2. 請求書入力
- order_screen.dart: /S3. 受発注入力
- sales_return_screen.dart: /S5. 売上返品入力
- sales_screen.dart: /S4. 売上入力(レジ)
- product_master_screen.dart: /M1. 商品マスタ
- customer_master_screen.dart: /M2. 得意先マスタ
- supplier_master_screen.dart: /M3. 仕入先マスタ
- warehouse_master_screen.dart: /M4. 倉庫マスタ
- employee_master_screen.dart: /M5. 担当者マスタ

README.md にも画面 ID マッピングを明記
2026-03-10 16:33:07 +09:00

224 lines
No EOL
8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Version: 1.16 - 売上入力画面PDF 帳票生成簡易実装TODO コメント化)
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:convert';
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;
final NumberFormat _currencyFormatter = NumberFormat.currency(symbol: '¥', decimalDigits: 0);
// Database に売上データを保存
Future<void> saveSalesData() async {
if (saleItems.isEmpty || !mounted) return;
try {
final itemsJson = jsonEncode(saleItems.map((item) => {
'product_id': item.productId,
'product_name': item.productName,
'product_code': item.productCode,
'unit_price': item.unitPrice.round(),
'quantity': item.quantity,
'subtotal': (item.unitPrice * item.quantity).round(),
}));
final salesData = {
'id': DateTime.now().millisecondsSinceEpoch,
'customer_id': selectedCustomer?.id ?? 1,
'sale_date': DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
'total_amount': totalAmount.round(),
'tax_rate': 8,
'product_items': itemsJson,
};
final insertedId = await DatabaseHelper.instance.insertSales(salesData);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('✅ 売上データ保存完了'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2)),
);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('保存成功'),
content: Text('売上 ID: #$insertedId\n合計金額:${_currencyFormatter.format(totalAmount)}'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('❌ 保存エラー:$e'), backgroundColor: Colors.red),
);
}
}
// PDF 帳票生成ロジックは TODO に記述printing パッケージ使用)
Future<void> generateAndShareInvoice() async {
if (saleItems.isEmpty || !mounted) return;
try {
await saveSalesData();
if (!mounted) return;
// TODO: PDF ファイルを生成して共有するロジックを実装printing パッケージ使用)
// 簡易実装:成功メッセージのみ表示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('📄 売上明細が共有されました'), backgroundColor: Colors.green),
);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('共有エラー:$e'), backgroundColor: Colors.orange),
);
}
}
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));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('/S4. 売上入力(レジ)'), actions: [
IconButton(icon: const Icon(Icons.save), onPressed: saveSalesData,),
IconButton(icon: const Icon(Icons.share), onPressed: generateAndShareInvoice,),
PopupMenuButton<String>(
onSelected: (value) async {
if (value == 'invoice') await generateAndShareInvoice();
},
itemBuilder: (ctx) => [
PopupMenuItem(child: const Text('売上明細を共有'), value: 'invoice',),
],
),
]),
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('${_currencyFormatter.format(totalAmount)}', 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} / ${_currencyFormatter.format(item.totalAmount)}'),
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,
});
}