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:
parent
8951016ad9
commit
cdc037bf21
4 changed files with 540 additions and 104 deletions
85
lib/models/customer.dart
Normal file
85
lib/models/customer.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.add),
|
||||
onPressed: () => _showAddDialog(context),
|
||||
),
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers,),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: [
|
||||
// ヘッダー
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'得意先名称',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
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: Icon(Icons.add, color: Theme.of(context).primaryColor),
|
||||
onPressed: () => _showAddDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// カードリスト形式(標準 Material 部品)
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: 5, // デモ用データ数
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: _customers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.teal.shade100,
|
||||
child: Icon(Icons.person, color: Colors.teal),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
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.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}%'),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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, '登録機能(プレースホルダ)'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '代表者名',
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('顧客名称 *'),
|
||||
subtitle: const Text('株式会社〇〇'),
|
||||
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '住所',
|
||||
hintText: '〒000-0000 北海道...',
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('電話番号'),
|
||||
subtitle: const Text('03-1234-5678'),
|
||||
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '電話番号',
|
||||
hintText: '0123-456789',
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
ListTile(
|
||||
title: const Text('Email'),
|
||||
subtitle: const Text('example@example.com'),
|
||||
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
|
||||
),
|
||||
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}株式会社を削除しますか?'),
|
||||
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('キャンセル'),
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
139
lib/services/database_helper.dart
Normal file
139
lib/services/database_helper.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
16
pubspec.yaml
16
pubspec.yaml
|
|
@ -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'
|
||||
|
|
@ -11,25 +11,21 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.6
|
||||
|
||||
|
||||
# SQLite データ永続化
|
||||
sqflite: ^2.3.3
|
||||
path_provider: ^2.1.1
|
||||
|
||||
|
||||
# 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:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/
|
||||
uses-material-design: true
|
||||
Loading…
Reference in a new issue