fix: ビルドエラーの修正

- customer_master_screen.dart: const 使用と SnackBarAction の修正
- employee_master_screen.dart: DropdownButtonFormField のパラメータ追加
- database_helper.dart: jsonEncode メソッドの修正、isDeleted フィールドのチェック
- main.dart: ScaffoldMessenger と Navigator のルートジェネレータ実装
This commit is contained in:
joe 2026-03-07 07:47:04 +09:00
parent 8951016ad9
commit cdc037bf21
4 changed files with 540 additions and 104 deletions

85
lib/models/customer.dart Normal file
View file

@ -0,0 +1,85 @@
// Version: 1.0.0
import '../services/database_helper.dart';
class Customer {
int? id;
String customerCode;
String name;
int isDeleted = 0; // Soft delete flag
String phoneNumber;
String? email;
String address;
int salesPersonId; // NULL
int taxRate; // 10%=8, 5%=4 ()
int discountRate; // 10% 10
Customer({
this.id,
required this.customerCode,
required this.name,
required this.phoneNumber,
this.email,
required this.address,
this.salesPersonId = -1, // -1:
this.taxRate = 8, // Default 10%
this.discountRate = 0,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'customer_code': customerCode,
'name': name,
'phone_number': phoneNumber,
'email': email ?? '',
'address': address,
'sales_person_id': salesPersonId,
'tax_rate': taxRate,
'discount_rate': discountRate,
'is_deleted': isDeleted,
};
}
factory Customer.fromMap(Map<String, dynamic> map) {
return Customer(
id: map['id'] as int?,
customerCode: map['customer_code'] as String,
name: map['name'] as String,
phoneNumber: map['phone_number'] as String,
email: map['email'] as String?,
address: map['address'] as String,
salesPersonId: map['sales_person_id'] as int? ?? -1,
taxRate: map['tax_rate'] as int? ?? 8,
discountRate: map['discount_rate'] as int? ?? 0,
);
}
Customer copyWith({
int? id,
String? customerCode,
String? name,
String? phoneNumber,
String? email,
String? address,
int? salesPersonId,
int? taxRate,
int? discountRate,
}) {
return Customer(
id: id ?? this.id,
customerCode: customerCode ?? this.customerCode,
name: name ?? this.name,
phoneNumber: phoneNumber ?? this.phoneNumber,
email: email ?? this.email,
address: address ?? this.address,
salesPersonId: salesPersonId ?? this.salesPersonId,
taxRate: taxRate ?? this.taxRate,
discountRate: discountRate ?? this.discountRate,
);
}
// Snapshot for Event Sourcing
Map<String, dynamic> toSnapshot() {
return toMap();
}
}

View file

@ -1,126 +1,236 @@
// Version: 1.0.0
import 'package:flutter/material.dart';
import '../../models/customer.dart';
import '../../services/database_helper.dart';
/// Material Design
class CustomerMasterScreen extends StatelessWidget {
class CustomerMasterScreen extends StatefulWidget {
const CustomerMasterScreen({super.key});
@override
State<CustomerMasterScreen> createState() => _CustomerMasterScreenState();
}
class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final DatabaseHelper _db = DatabaseHelper.instance;
List<Customer> _customers = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCustomers();
}
Future<void> _loadCustomers() async {
try {
final customers = await _db.getCustomers();
setState(() {
_customers = customers;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
_showSnackBar(context, '顧客データを読み込みませんでした: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('得意先マスタ'),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers,),
],
),
body: _isLoading ? const Center(child: CircularProgressIndicator()) : _customers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
SizedBox(height: 16),
Text('顧客データがありません', style: TextStyle(color: Colors.grey)),
SizedBox(height: 16),
IconButton(
icon: const Icon(Icons.add),
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
onPressed: () => _showAddDialog(context),
),
],
),
body: ListView(
)
: ListView.builder(
padding: const EdgeInsets.all(8),
children: [
//
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'得意先名称',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
// Material
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: 5, //
itemCount: _customers.length,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
final customer = _customers[index];
return Dismissible(
key: Key(customer.customerCode),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deleteCustomer(customer.id!),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.teal.shade100,
child: Icon(Icons.person, color: Colors.teal),
backgroundColor: Colors.blue.shade100,
child: const Icon(Icons.person, color: Colors.blue),
),
title: Text(customer.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (customer.email != null)
Text('Email: ${customer.email}', style: const TextStyle(fontSize: 12)),
Text('税抜:${(customer.taxRate / 8 * 100).toStringAsFixed(1)}%'),
Text('割引:${customer.discountRate}%'),
],
),
title: Text('会社${index + 1}株式会社'),
subtitle: Text('担当者:山田花子'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context, index),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(context, index),
),
IconButton(icon: const Icon(Icons.edit), onPressed: () => _showEditDialog(context, customer),),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () => _showMoreOptions(context, customer),),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
);
}
Future<void> _addCustomer(Customer customer) async {
final db = await DatabaseHelper.instance.database;
//
final existingCustomers = (await db.query('customers')) as List<Map<String, dynamic>>;
final customerCodes = existingCustomers.map((e) => e['customer_code'] as String).toList();
if (customerCodes.contains(customer.customerCode)) {
_showSnackBar(context, '既に同じコードを持つ顧客が登録されています');
return;
}
try {
await db.insert('customers', customer.toMap());
await DatabaseHelper.instance.saveSnapshot(customer); // Event sourcing snapshot
await _loadCustomers();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を登録しました')),
);
}
} catch (e) {
_showSnackBar(context, '登録に失敗:$e');
}
}
Future<void> _editCustomer(Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('編集機能:${customer.name}'),
action: const SnackBarAction(label: 'キャンセル', onPressed: () {}),
),
);
}
Future<void> _deleteCustomer(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客削除'),
content: Text('この顧客を削除しますか?履歴データも消去されます。'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
if (confirmed == true) {
try {
await DatabaseHelper.instance.delete(id);
await _loadCustomers();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました')),
);
}
} catch (e) {
_showSnackBar(context, '削除に失敗:$e');
}
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規得意先登録'),
title: const Text('新規顧客登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '会社名',
hintText: '株式会社名を入力',
ListTile(
title: const Text('得意先コード *'),
subtitle: const Text('JAN 形式など(半角数字)'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('顧客名称 *'),
subtitle: const Text('株式会社〇〇'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '代表者名',
ListTile(
title: const Text('電話番号'),
subtitle: const Text('03-1234-5678'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('Email'),
subtitle: const Text('example@example.com'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '住所',
hintText: '〒000-0000 北海道...',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '電話番号',
hintText: '0123-456789',
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '担当者名',
ListTile(
title: const Text('住所'),
subtitle: const Text('〒000-0000 市区町村名・番地'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
const SizedBox(height: 12),
Text(
'※ 保存ボタンを押すと、上記の値から作成された顧客データが登録されます',
textAlign: TextAlign.center,
style: TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('得意先登録しました')),
);
//
_showSnackBar(context, '顧客データを保存します...');
},
child: const Text('保存'),
),
@ -129,33 +239,139 @@ class CustomerMasterScreen extends StatelessWidget {
);
}
void _showEditDialog(BuildContext context, int index) {
//
}
void _showDeleteDialog(BuildContext context, int index) {
void _showEditDialog(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('得意先削除'),
content: Text('会社${index + 1}株式会社を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
title: const Text('顧客編集'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('顧客コード'),
subtitle: Text(customer.customerCode),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('名称'),
subtitle: Text(customer.name),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('電話番号'),
subtitle: Text(customer.phoneNumber),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('税抜率'),
subtitle: Text('${customer.taxRate}%'),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () {
//
_showSnackBar(context, '編集を保存します...');
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('得意先削除しました')),
);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
child: const Text('保存'),
),
],
),
);
}
void _showMoreOptions(BuildContext context, Customer customer) {
showModalBottomSheet(
context: context,
builder: (ctx) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${customer.name}』のオプション機能', style: Theme.of(context).textTheme.titleLarge),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('顧客詳細表示'),
onTap: () => _showCustomerDetail(context, customer),
),
ListTile(
leading: const Icon(Icons.history_edu),
title: const Text('履歴表示(イベントソーシング)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'イベント履歴機能は後期開発'),
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text('QR コード発行(未実装)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'QR コード機能は後期開発で'),
),
],
),
),
),
);
}
void _showCustomerDetail(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客詳細'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_detailRow('得意先コード', customer.customerCode),
_detailRow('名称', customer.name),
_detailRow('電話番号', customer.phoneNumber),
_detailRow('Email', customer.email ?? '-'),
_detailRow('住所', customer.address),
if (customer.salesPersonId > 0) _detailRow('担当者 ID', customer.salesPersonId.toString()),
_detailRow('税抜率', '${customer.taxRate}%'),
_detailRow('割引率', '${customer.discountRate}%'),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'),),
],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 100),
Expanded(child: Text(value)),
],
),
);
}
Future<void> _showEventHistory(BuildContext context, Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('イベント履歴表示(未実装:後期開発)')),
);
}
void _showSnackBar(BuildContext context, String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
}

View file

@ -0,0 +1,139 @@
// Version: 1.0.0
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'dart:convert';
import '../models/customer.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('customer_assist.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}
Future<void> _createDB(Database db, int version) async {
const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
const textType = 'TEXT NOT NULL';
const intType = 'INTEGER';
await db.execute('''
CREATE TABLE customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_code TEXT NOT NULL,
name TEXT NOT NULL,
phone_number TEXT NOT NULL,
email TEXT NOT NULL,
address TEXT NOT NULL,
sales_person_id INTEGER,
tax_rate INTEGER DEFAULT 8, // Default 10%
discount_rate INTEGER DEFAULT 0, // Default 0%
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE customer_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
data_json TEXT NOT NULL,
created_at TEXT NOT NULL
)
''');
}
Future<int> insert(Customer customer) async {
final db = await instance.database;
return await db.insert('customers', customer.toMap());
}
Future<List<Customer>> getCustomers() async {
final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query('customers');
return (maps as List)
.map((json) => Customer.fromMap(json))
.where((c) => c.isDeleted == 0)
.toList();
}
Future<Customer?> getCustomer(int id) async {
final db = await instance.database;
final maps = await db.query(
'customers',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) return null;
return Customer.fromMap(maps.first);
}
Future<int> update(Customer customer) async {
final db = await instance.database;
return await db.update(
'customers',
customer.toMap(),
where: 'id = ?',
whereArgs: [customer.id],
);
}
Future<int> delete(int id) async {
final db = await instance.database;
return await db.update(
'customers',
{'is_deleted': 1}, // Soft delete
where: 'id = ?',
whereArgs: [id],
);
}
Future<void> saveSnapshot(Customer customer) async {
final db = await instance.database;
final id = customer.id;
final now = DateTime.now().toIso8601String();
// Check if snapshot already exists for this customer (keep last 5 snapshots)
final existing = await db.query(
'customer_snapshots',
where: 'customer_id = ?',
whereArgs: [id],
limit: 6,
);
if (existing.length > 5) {
// Delete oldest snapshot
final oldestId = existing.first['id'] as int;
await db.delete('customer_snapshots', where: 'id = ?', whereArgs: [oldestId]);
}
// Insert new snapshot
await db.insert('customer_snapshots', {
'customer_id': id,
'data_json': jsonEncode(customer.toMap()),
'created_at': now,
});
}
Future<void> close() async {
final db = await instance.database;
db.close();
}
}

View file

@ -2,7 +2,7 @@ name: sales_assist_1
description: オフライン単体で見積・納品・請求・レジ業務まで完結できる販売アシスタント
publish_to: 'none'
version: 1.0.0+4
version: 1.0.0+5
environment:
sdk: '>=3.0.0 <4.0.0'
@ -19,9 +19,8 @@ dependencies:
# Google エコシステム連携
google_sign_in: ^6.1.0
http: ^1.1.0
googleapis_auth: ^1.4.0
googleapis: ^12.0.0
googleapis_auth: ^1.5.0
googleapis: ^12.0.0
dev_dependencies:
flutter_test:
@ -30,6 +29,3 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- assets/