From 8f1df14b7b5fbdde3bf135b18b99be98903523fa Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 20:01:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=85=E5=BD=93=20=E3=83=9E=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0\n\n-=20l?= =?UTF-8?q?ib/models/employee.dart:=20Employee=20=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E5=AE=9A=E7=BE=A9\n-=20lib/widgets/employee=5Fedit=5F?= =?UTF-8?q?dialog.dart:=20=E5=BE=93=E6=A5=AD=E5=93=A1=E7=B7=A8=E9=9B=86?= =?UTF-8?q?=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0\n-=20lib/screens/?= =?UTF-8?q?master/employee=5Fmaster=5Fscreen.dart:=20=E6=8B=85=E5=BD=93?= =?UTF-8?q?=E8=80=85=E3=83=9E=E3=82=B9=E3=82=BF=E7=94=BB=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flutter-plugins-dependencies | 2 +- .../master/employee_master_screen.dart | 198 +++++++++++ .../lib/widgets/employee_edit_dialog.dart | 257 ++++++++++++++ docs/short_term_plan.md | 298 ++++++---------- lib/models/employee.dart | 63 ++++ lib/models/sample_employee.dart | 63 ++++ .../master/customer_master_screen.dart | 77 +--- .../master/employee_master_screen.dart | 247 +++++++------ lib/screens/master/product_master_screen.dart | 86 ++--- .../master/supplier_master_screen.dart | 22 +- lib/services/database_helper.dart | 99 ++---- lib/widgets/employee_edit_dialog.dart | 329 ++++++++++++++++++ lib/widgets/master_edit_dialog.dart | 299 +++++++++++++--- 13 files changed, 1466 insertions(+), 574 deletions(-) create mode 100644 @workspace/lib/screens/master/employee_master_screen.dart create mode 100644 @workspace/lib/widgets/employee_edit_dialog.dart create mode 100644 lib/models/employee.dart create mode 100644 lib/models/sample_employee.dart create mode 100644 lib/widgets/employee_edit_dialog.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 961dd7c..632013a 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_sign_in_ios","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_ios","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13+6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/home/user/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.33/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.9/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13+14/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"file_selector_macos","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2+1/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"file_selector_linux","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":false,"dependencies":["url_launcher_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"file_selector_windows","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":["url_launcher_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"google_sign_in_web","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.2/","dependencies":[],"dev_dependency":false},{"name":"image_picker_for_web","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.1/","dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","dependencies":["url_launcher_web"],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"printing","dependencies":[]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-11 12:59:45.393067","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_sign_in_ios","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_ios","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13+6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/home/user/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.33/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.9/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13+14/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"file_selector_macos","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2+1/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/user/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"file_selector_linux","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":false,"dependencies":["url_launcher_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"file_selector_windows","path":"/home/user/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/user/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":["url_launcher_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"google_sign_in_web","path":"/home/user/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.2/","dependencies":[],"dev_dependency":false},{"name":"image_picker_for_web","path":"/home/user/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.1/","dependencies":[],"dev_dependency":false},{"name":"printing","path":"/home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/","dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/user/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","dependencies":["url_launcher_web"],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/user/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"printing","dependencies":[]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-11 19:54:51.629173","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/@workspace/lib/screens/master/employee_master_screen.dart b/@workspace/lib/screens/master/employee_master_screen.dart new file mode 100644 index 0000000..d7a50d2 --- /dev/null +++ b/@workspace/lib/screens/master/employee_master_screen.dart @@ -0,0 +1,198 @@ +// Version: 2.0 - 担当者マスタ画面(リッチ編集ダイアログ統合) +// ※ EmployeeEditDialog を使用した簡易実装 + +import 'package:flutter/material.dart'; +import '../models/employee.dart'; +import '../widgets/employee_edit_dialog.dart'; + +/// 担当者マスタ管理画面 +class EmployeeMasterScreen extends StatefulWidget { + const EmployeeMasterScreen({super.key}); + + @override + State createState() => _EmployeeMasterScreenState(); +} + +class _EmployeeMasterScreenState extends State { + List _employees = []; + bool _loading = true; + + // 検索キーワード + String get _filteredEmployees => _searchKeyword.isEmpty ? _employees : + _employees.where((e) => e.name.toLowerCase().contains(_searchKeyword.toLowerCase()) || + (e.department.isNotEmpty && e.department.toLowerCase().contains(_searchKeyword.toLowerCase()))).toList(); + + @override + void initState() { + super.initState(); + _loadEmployees(); + } + + /// 従業員データをロード(デモデータ) + Future _loadEmployees() async { + setState(() => _loading = true); + try { + // サンプルデータを初期化 + final demoData = [ + Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-1234-5678', department: '営業部', role: '営業担当'), + Employee(id: 2, name: '田中花子', email: 'tanaka@company.com', tel: '03-2345-6789', department: '総務部', role: '総務担当'), + Employee(id: 3, name: '鈴木一郎', email: 'suzuki@company.com', tel: '03-3456-7890', department: '経理部', role: '経理担当'), + ]; + setState(() => _employees = demoData); + } catch (e) { + if (mounted) ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), + ); + } finally { + setState(() => _loading = false); + } + } + + /// 新規従業員追加 + Future _addEmployee() async { + final edited = await showDialog( + context: context, + builder: (ctx) => EmployeeEditDialog( + title: '担当者登録', + initialData: null, + ), + ); + + if (edited != null && mounted) { + setState(() => _employees.add(edited)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('担当者登録完了'), backgroundColor: Colors.green), + ); + } + } + + /// 従業員編集 + Future _editEmployee(Employee employee) async { + final edited = await showDialog( + context: context, + builder: (ctx) => EmployeeEditDialog( + title: '担当者編集', + initialData: employee, + ), + ); + + if (edited != null && mounted) { + setState(() { + _employees = _employees.map((e) => e.id == edited.id ? edited : e).toList(); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('担当者更新完了'), backgroundColor: Colors.green), + ); + } + } + + /// 従業員削除 + Future _deleteEmployee(Employee employee) async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('担当者削除'), + content: Text('この担当者を実際に削除しますか?'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: const Text('削除'), + ), + ], + ), + ); + + if (confirmed == true && mounted) { + setState(() { + _employees.removeWhere((e) => e.id == employee.id); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('担当者削除完了'), backgroundColor: Colors.green), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('/M5. 担当者マスタ'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), + IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee), + ], + ), + body: Column( + children: [ + // 検索バー + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration( + hintText: '担当者名で検索...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (value) => setState(() => _searchKeyword = value), + ), + ), + // 一覧リスト + Expanded( + child: _loading ? const Center(child: CircularProgressIndicator()) : + _filteredEmployees.isEmpty ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.person_outline, size: 64, color: Colors.grey[300]), + SizedBox(height: 16), + Text('担当者データがありません', style: TextStyle(color: Colors.grey)), + SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _addEmployee, + icon: const Icon(Icons.add), + label: const Text('新規登録'), + ), + ], + ), + ) : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: _filteredEmployees.length, + itemBuilder: (context, index) { + final employee = _filteredEmployees[index]; + return Card( + margin: EdgeInsets.zero, + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.purple.shade100, + child: Text('${employee.department.substring(0, 1)}', style: const TextStyle(fontWeight: FontWeight.bold)), + ), + title: Text(employee.name ?? '未入力', style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (employee.department.isNotEmpty) Text('部署:${employee.department}', style: const TextStyle(fontSize: 12)), + if (employee.role.isNotEmpty) Text('役職:${employee.role}', style: const TextStyle(fontSize: 12)), + if (employee.tel.isNotEmpty) Text('TEL: ${employee.tel}', style: const TextStyle(fontSize: 10, color: Colors.grey)), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(icon: const Icon(Icons.edit), onPressed: () => _editEmployee(employee)), + IconButton(icon: const Icon(Icons.delete_outline), onPressed: () => _deleteEmployee(employee)), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/@workspace/lib/widgets/employee_edit_dialog.dart b/@workspace/lib/widgets/employee_edit_dialog.dart new file mode 100644 index 0000000..c703639 --- /dev/null +++ b/@workspace/lib/widgets/employee_edit_dialog.dart @@ -0,0 +1,257 @@ +// 従業員編集ダイアログ(リッチ版) +// ※ Employee モデルに特化した編集用ダイアログ + +import 'package:flutter/material.dart'; +import '../models/employee.dart'; + +/// 従業員用のリッチな編集ダイアログ +class EmployeeEditDialog extends StatefulWidget { + final String title; + final Employee? initialData; // null = 新規作成 + final void Function(Employee) onSave; // 保存コールバック + + const EmployeeEditDialog({ + super.key, + required this.title, + this.initialData, + required this.onSave, + }); + + @override + State createState() => _EmployeeEditDialogState(); +} + +class _EmployeeEditDialogState extends State { + late TextEditingController nameController; + late TextEditingController emailController; + late TextEditingController telController; + late TextEditingController departmentController; + late TextEditingController roleController; + + @override + void initState() { + super.initState(); + final data = widget.initialData; + nameController = TextEditingController(text: data?.name ?? ''); + emailController = TextEditingController(text: data?.email ?? ''); + telController = TextEditingController(text: data?.tel ?? ''); + departmentController = TextEditingController(text: data?.department ?? ''); + roleController = TextEditingController(text: data?.role ?? ''); + } + + @override + void dispose() { + nameController.dispose(); + emailController.dispose(); + telController.dispose(); + departmentController.dispose(); + roleController.dispose(); + super.dispose(); + } + + /// リッチな入力フィールドビルダー(共通) + Widget _buildRichTextField( + String label, + TextEditingController controller, { + TextInputType? keyboard, + IconData? icon, + String hint = '', + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: controller, + keyboardType: keyboard, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: hint.isEmpty ? null : hint, + prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + child: Container( + constraints: const BoxConstraints(maxWidth: 420), + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // タイトル + Row( + children: [ + Icon(Icons.person, size: 20, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded(child: Text( + widget.title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + IconButton( + icon: Icon(Icons.close, color: Colors.grey), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 12), + + // ヒントテキスト + Center( + child: Text( + '新規作成の場合は「空白」から入力して OK を押してください', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic), + ), + ), + const SizedBox(height: 8), + + // リッチな編集フォーム + Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Theme.of(context).dividerColor), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 基本情報セクション + Text( + '■ 基本情報', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + // 名前フィールド + _buildRichTextField( + '氏名 *', + nameController, + keyboard: TextInputType.name, + icon: Icons.person, + hint: e.g., '山田太郎', + ), + + // メールアドレスフィールド + _buildRichTextField( + 'E メール *', + emailController, + keyboard: TextInputType.emailAddress, + icon: Icons.email, + hint: 'example@company.com', + ), + + // 電話番号フィールド + _buildRichTextField( + '電話番号 *', + telController, + keyboard: TextInputType.phone, + icon: Icons.phone, + hint: '03-1234-5678', + ), + + const Divider(), + + // 部署情報セクション + Text( + '■ 部署・役職', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + // 部門フィールド + _buildRichTextField( + '部署 *', + departmentController, + keyboard: TextInputType.text, + icon: Icons.business, + hint: '営業部', + ), + + // 役職フィールド + _buildRichTextField( + '役職 *', + roleController, + keyboard: TextInputType.text, + icon: Icons.badge, + hint: '営業担当', + ), + ], + ), + ), + + const SizedBox(height: 16), + + // アクションボタン(Flex で配置) + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' キャンセル ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 3, // より広いボタン + child: ElevatedButton( + onPressed: () { + final employee = Employee( + id: widget.initialData?.id ?? -1, + name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text, + email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text, + tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text, + department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text, + role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text, + ); + widget.onSave(employee); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' 保存 ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/docs/short_term_plan.md b/docs/short_term_plan.md index d618b5c..5cab048 100644 --- a/docs/short_term_plan.md +++ b/docs/short_term_plan.md @@ -1,233 +1,129 @@ -# 短期計画(Sprint Plan)- H-1Q プロジェクト +# 少プロジェクト短期実装計画(担当者に限定版) -## 1. スプリント概要 +## 1. プロジェクト概要 -| 項目 | 内容 | -|---|---| -| **開発コード** | **H-1Q(販売アシスト 1 号)**✅NEW | -| **スプリント期間** | **2026/03/09 - 2026/03/23 → Sprint 5(H-1Q-S4 完了)** ✅
**Sprint 6: 2026/04/01-2026/04/15 → H-1Q-Sprint 6-7 移行中** 🔄 | -| **目標** | **見積機能完結 + 売上入力画面基本動作 + PDF 帳票出力対応** ✅
**請求転換 UI 実装完了** ✅
**在庫管理モジュール UI 実装完了** ✅(H-1Q-Sprint 6) | -| **優先度** | 🟢 High → H-1Q-Sprint 5-6 移行中 | +**目標**: 担当者マスタ画面のリッチ編集機能を実現し、販売・仕入れ業務との連携を整える + +**期間**: 1-2 ヶ月程度で MVP をリリース +**優先度**: 担当者マスタ → サンプルデータ → ビルド検証 --- -## 2. タスクリスト +## 2. ワークフロー -### 2.1 **Sprint 4: コア機能強化(完了)** ✅✅H-1Q - -#### 📦 見積入力機能完了 ✅✅H-1Q - -- [x] DatabaseHelper 接続(estimate テーブル CRUD API) -- [x] EstimateScreen の基本実装(得意先選択・商品追加) -- [x] 見積保存時のエラーハンドリング完全化 -- [x] PDF 帳票出力テンプレート準備✅NEW -- [x] **`insertEstimate(Estimate estimate)`の Model ベース実装**✅NEW -- [x] **`estimates` テーブルの product_items, status, expiry_date フィールド追加**✅NEW - -**担当者**: Sales チーム -**工期**: 3/15-3/20 → **H-1Q-Sprint 4 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High → H-1Q-Sprint 5 移行✅ - -#### 🧾 売上入力機能実装 - DocumentDirectory 自動保存対応 ✅✅H-1Q - -- [x] `sales_screen.dart` の PDF 出力ボタン実装 -- [x] JAN コード検索ロジックの実装✅NEW -- [x] DatabaseHelper で Sales テーブルへの INSERT 処理✅NEW -- [x] 合計金額・税額計算ロジック✅NEW -- [x] DocumentDirectory への自動保存ロジック実装✅完了 - -**担当**: 販売管理チーム -**工期**: 3/18-3/25 → **H-1Q-Sprint 4 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High → H-1Q-Sprint 5 移行✅ - -#### 💾 インベントリ機能実装 - Sprint 6 完了🔄✅H-1Q - -- [x] Inventory モデル定義(lib/models/inventory.dart)✅NEW -- [x] DatabaseHelper に inventory テーブル追加(version: 3)✅NEW -- [x] insertInventory/getInventory/updateInventory/deleteInventory API✅NEW -- [x] 在庫テストデータの自動挿入✅NEW - -**担当**: Sales チーム -**工期**: 3/08-3/15 → **H-1Q-Sprint 6 で完了(2026/03/09)** 🔄 -**優先度**: 🟢 High (H-1Q-Sprint 6)✅ - -#### 💰 **見積→請求転換機能実装** ✅✅H-1Q - -- [x] `createInvoiceTable()` の API 実装✅NEW -- [x] `convertEstimateToInvoice(Estimate)` の実装ロジック✅NEW -- [x] Invoice テーブルのテーブル定義と CRUD API✅NEW -- [x] Estimate の status フィールドを「converted」に更新✅NEW -- [x] UI: estimate_screen.dart に転換ボタン追加(完了済み)✅ - -**担当**: Database チーム -**工期**: 3/16-3/20 → **H-1Q-Sprint 5 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High → H-1Q-Sprint 5-M1 移行✅ - ---- - -## 6. タスク完了ログ(**H-1Q-Sprint 4 完了:2026/03/09**)✅✅NEW - -### ✅ 完了タスク一覧✅H-1Q - -#### 📄 PDF 帳票出力機能実装 ✅✅H-1Q - -- [x] flutter_pdf_generator パッケージ導入 -- [x] sales_invoice_template.dart のテンプレート定義✅NEW -- [x] A5 サイズ・ヘッダー/フッター統一デザイン✅NEW -- [x] DocumentDirectory への自動保存ロジック実装(優先中)✅完了 - -**担当**: UI/UX チーム -**工期**: 3/10-3/14 → **H-1Q-Sprint 4 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High - -#### 💾 Inventory 機能実装 ✅🔄✅H-1Q - -- [x] Inventory モデル定義(lib/models/inventory.dart)✅NEW -- [x] DatabaseHelper に inventory テーブル追加✅NEW -- [x] CRUD API 実装(insert/get/update/delete)✅NEW - -**担当**: Sales チーム -**工期**: 3/08-3/15 → **H-1Q-Sprint 6 で完了(2026/03/09)** ✅🔄 -**優先度**: 🟢 High - -#### 💾 **見積機能完全化** ✅✅H-1Q - -- [x] `insertEstimate(Estimate estimate)` の Model ベース実装✅NEW -- [x] `_encodeEstimateItems()` ヘルパー関数実装✅NEW -- [x] JSON エンコード/デコードロジックの完全化✅NEW -- [x] `getEstimate/insertEstimate/updateEstimate/deleteEstimate` 全体機能✅NEW - -**担当**: Database チーム -**工期**: 3/09-3/16 → **H-1Q-Sprint 4 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High - -#### 🧾 売上入力画面完全実装 ✅✅H-1Q - -- [x] `sales_screen.dart` の PDF 出力ボタン実装 -- [x] JAN コード検索ロジックの実装 -- [x] DatabaseHelper で Sales テーブルへの INSERT 処理 -- [x] 合計金額・税額計算ロジック -- [x] DocumentDirectory への自動保存ロジック実装✅完了 - -**担当**: 販売管理チーム -**工期**: 3/18-3/25 → **H-1Q-Sprint 4 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High - -#### 💰 **見積→請求転換機能実装** ✅✅H-1Q - -- [x] `createInvoiceTable()` の API 実装 -- [x] `convertEstimateToInvoice(Estimate)` の実装ロジック -- [x] Invoice テーブルのテーブル定義と CRUD API -- [x] Estimate の status フィールドを「converted」に更新✅NEW - -**担当**: Database チーム -**工期**: 3/16-3/20 → **H-1Q-Sprint 5 で完了(2026/03/09)** ✅ -**優先度**: 🟢 High - -#### 🎯 **見積→請求転換 UI(H-1Q-Sprint 4)実装** ✅✅NEW - -- [x] estimate_screen.dart に転換ボタン追加✅NEW -- [x] DatabaseHelper.insertInvoice API の重複チェック実装✅NEW -- [x] Estimate から Invoice へのデータ転換ロジック実装✅NEW -- [x] UI: 転換完了通知 + 請求書画面遷移案内✅NEW - -**担当**: Estimate チーム -**工期**: **2026/03/09(H-1Q-Sprint 4 移行)で完了** ✅ -**優先度**: 🟢 High → H-1Q-Sprint 5-M1 移行✅ - ---- - -## 7. 依存関係 ```mermaid -graph LR - A[見積機能完了] -->|完了時 | B[売上入力実装] - B -->|完了時 | C[請求作成設計] - C -->|完了時 | D[テスト環境構築] - A -.->|PDF テンプレート共有 | E[sales_invoice_template.dart] +graph TD + A[担当者マスタ画面] --> B[MasterEditDialog 作成] + B --> C[sample_employee.dart 定義] + C --> D[employee_master_screen.dart リッチ化] + D --> E[サンプルデータ追加] + E --> F[ビルド検証] ``` -**要件**: -- ✅ 見積保存が正常動作(DatabaseHelper.insertEstimate)✅NEW -- ✅ 売上テーブル定義と INSERT API -- ✅ PDF ライブラリ選定:flutter_pdfgenerator -- ✅ 売上伝票テンプレート設計完了✅NEW -- ✅ **請求転換 UI 実装済み(H-1Q-Sprint 4)** ✅NEW +--- + +## 3. 実装順序 + +### フェーズ 1: 編集ダイアログの整備 (1-2 週間) +1. `MasterEditDialog` を共有ライブラリとして作成 + - TextFormField で全てのフィールドを編集可能 + - 保存/キャンセルボタン付き + - 無効な場合のバリデーション表示 + +2. `sample_employee.dart` にサンプルデータ追加 + - 初期担当者データ(5-10 件程度) + - employee_id, name, email, tel, department, role + +### フェーズ 2: マスタ画面の連携 (2-3 週間) +3. `employee_master_screen.dart` のリッチ化 + - MasterEditDialog で編集画面を表示 + - リストビューに編集ボタン付き + - 追加ダイアログを統合 + +4. シンプルなリスト管理から開始 + - ListView.builder で担当者一覧表示 + - Card に編集ボタンを追加 + +### フェーズ 3: 業務連携の準備 (1-2 週間) +5. 販売画面への担当者紐付機能 +6. 仕入れ画面への担当者紐付機能 +7. 簡易な在庫管理と売上照会 --- -## 8. **Sprint 5 完了レポート:2026/03/09** ✅✅H-1Q +## 4. テックスタック -### 📋 完了タスク一覧 -- ✅ 見積→請求転換 UI(estimate_screen.dart に転換ボタン追加)✅ -- ✅ Invoice テーブル CRUD API(insert/get/update/delete)✅ -- ✅ DocumentDirectory 自動保存機能実装✅ -- ✅ Inventory モデル定義完了✅ - -### 📊 進捗状況 -- **完了**: **85%**(請求転換 UI + 在庫モデル + DocumentDirectory)✅H-1Q -- **進行中**: クラウド同期要件定義🔄 -- **未着手**: PDF 領収書テンプレート⏳ +| カテゴリ | ツール | +|---------|--------| +| State Management | setState (シンプル) | +| フォーム編集 | TextField + TextEditingController | +| ダイアログ | AlertDialog で標準ダイアログ利用 | +| データ永続化 | 当面はメモリ保持(後日 Sqflite) | +| ロギング | 簡易な print 出力 | --- -## 9. **Sprint 6: H-1Q(2026/04/01-2026/04/15)** ✅🔄 +## 5. デリべラブル -### 📋 タスク予定 -1. **見積→請求転換機能**の検証完了 ✅(H-1Q-Sprint 4 で完了) -2. **Inventory モデル定義と DatabaseHelper API**完全化✅完了(H-1Q-Sprint 6) -3. **PDF 領収書テンプレート**の設計開始⏳将来目標 -4. **クラウド同期ロジック**の要件定義⏳計画延期 - -### 🎯 **Sprint 6 ミルストーン:H-1Q-S6-M1(在庫管理完了)**📅✅ -**目標**: **在庫管理 UI の実装完了** ✅(H-1Q-Sprint 6 完了) -**優先度**: 🟢 High - -### 📅 開発スケジュール H-1Q -- **Week 8 (3/09)**: **見積→請求転換 UI**(完了✅) -- **Week 9 (3/16)**: **クラウド同期ロジック設計🔄延期中** -- **Week 10 (3/23)**: Conflict Resolution 実装⏳計画延期 +- [x] `MasterEditDialog` の実装 +- [ ] `sample_employee.dart` のサンプルデータ追加 +- [x] `employee_master_screen.dart` の簡素リスト実装(完了) +- [ ] リッチ編集画面の実装 +- [ ] ビルドと動作確認 --- -## 4. リスク管理 +## 6. 定義済みインターフェース -| リスク | 影響 | 確率 | 対策 | -|---|-|---|--| -| 見積保存エラー | 高 | 🔴 中 | エラーハンドリング完全化(既実装)✅NEW -| PDF ライブラリ互換性 | 中 | 🟡 低 | flutter_pdfgenerator の A5 対応確認済 ✅H-1Q -| DatabaseHelper API コスト | 低 | 🟢 低 | 既存スクリプト・テンプレート再利用 ✅H-1Q -| sales_screen.dart パフォーマンス | 中 | 🟡 中 | Lazy loading / ページネーション導入検討 +### MasterEditDialog インターフェース: +```dart +class MasterEditDialog { + final String title; + final Map initialData; // editMode の時だけ使用 + final Future Function(Map) saveCallback; + + static const String idKey = 'id'; + static const String nameKey = 'name'; + static const String emailKey = 'email'; + static const String telKey = 'tel'; +} +``` + +### sample_employee.dart の形式: +```dart +class SampleEmployee { + final int id; + final String name; + final String email; + final String tel; + final String department; + final String role; + + // factory で作成可能 + + Map toJson() => {...}; +} +``` --- -## 5. 進捗追跡方法 +## 7. ビルド検証手順 -**チェックリスト方式**: -- [x] タスク完了 → GitHub Commit で記録(`feat: XXX`)✅H-1Q -- [x] マークオフ → README.md の実装完了セクション更新 ✅H-1Q - -**デイリー報告 H-1Q**: -- 朝会(09:30)→ チェックリストの未着手項目確認 ✅H-1Q -- 夕戻り(17:30)→ 本日のコミット数報告 ✅H-1Q +1. `flutter build apk --debug` でビルド +2. Android エミュレータまたは物理デバイスで動作確認 +3. マスタ登録・編集のフローテスト +4. 画面遷移の確認 --- -## 7. スプリントレビュー項目(木曜 15:00) +## 8. リスク管理 -### レビューアジェンダ H-1Q -1. **実装成果物**: CheckList の完了項目確認✅H-1Q -2. **課題共有**: 未完成タスクの原因分析🔄延期 -3. **次スプリント計画**: **Sprint 6 タスク定義**(H-1Q-Sprint 6: 在庫管理完了)✅ -4. **ステークホルダー報告**: プロジェクト計画書の更新 ✅H-1Q - -### レビュー資料準備 H-1Q -- README.md(実装完了セクション)✅NEW -- project_plan.md(M1-M3 マイルストーン記録)✅H-1Q -- test/widget_test.dart(テストカバレッジレポート) -- sales_invoice_template.dart(PDF テンプレート設計書)✅NEW -- **`lib/services/database_helper.dart`**(見積・請求 API 設計書)✅H-1Q +- **State Management の複雑化**: setState を使いすぎると再描画が増える → 最小限に抑える +- **データ永続化なし**: アプリ再起動で失われる → MVP で OK、後日改善 +- **サンプルデータ不足**: ユーザーに手入力させる → コード内で初期化 --- -**最終更新**: **2026/03/09** -**バージョン**: **1.7** (請求転換 UI + H-1Q-Sprint 5 移行完了) ✅NEW \ No newline at end of file +## 9. まとめ + +担当者のみから着手し、マスター管理機能とサンプルデータを整備。その後に他のマスタ画面を順次実装する方針で進める。 \ No newline at end of file diff --git a/lib/models/employee.dart b/lib/models/employee.dart new file mode 100644 index 0000000..9cc221e --- /dev/null +++ b/lib/models/employee.dart @@ -0,0 +1,63 @@ +// Version: 1.0 - Employee モデル定義(簡易 POJO) +class Employee { + final int? id; + final String name; + final String email; + final String tel; + final String department; + final String role; + final DateTime createdAt; + + Employee({ + this.id, + required this.name, + required this.email, + required this.tel, + required this.department, + required this.role, + this.createdAt = DateTime.now(), + }); + + // JSON シリアライゼーション(簡素版) + Map toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'tel': tel, + 'department': department, + 'role': role, + 'created_at': createdAt.toIso8601String(), + }; + + // JSON デシリアライゼーション(簡素版) + factory Employee.fromJson(Map json) => Employee( + id: json['id'] as int?, + name: json['name'] as String, + email: json['email'] as String, + tel: json['tel'] as String, + department: json['department'] as String, + role: json['role'] as String, + ); + + // 深コピー用メソッド(編集時の使用) + Employee copyWith({ + int? id, + String? name, + String? email, + String? tel, + String? department, + String? role, + DateTime? createdAt, + }) => Employee( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + tel: tel ?? this.tel, + department: department ?? this.department, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + ); + + @override + String toString() => 'Employee(id: $id, name: $name)'; +} diff --git a/lib/models/sample_employee.dart b/lib/models/sample_employee.dart new file mode 100644 index 0000000..80edca7 --- /dev/null +++ b/lib/models/sample_employee.dart @@ -0,0 +1,63 @@ +// Version: 1.0 - Employee モデル定義(簡易 POJO) +class Employee { + final int? id; + final String name; + final String email; + final String tel; + final String department; + final String role; + final DateTime createdAt; + + Employee({ + this.id, + required this.name, + required this.email, + required this.tel, + required this.department, + required this.role, + this.createdAt = DateTime.now(), + }); + + // JSON シリアライゼーション(簡素版) + Map toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'tel': tel, + 'department': department, + 'role': role, + 'created_at': createdAt.toIso8601String(), + }; + + // JSON デシリアライゼーション(簡素版) + factory Employee.fromJson(Map json) => Employee( + id: json['id'] as int?, + name: json['name'] as String, + email: json['email'] as String, + tel: json['tel'] as String, + department: json['department'] as String, + role: json['role'] as String, + ); + + // 深コピー用メソッド(編集時の使用) + Employee copyWith({ + int? id, + String? name, + String? email, + String? tel, + String? department, + String? role, + DateTime? createdAt, + }) => Employee( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + tel: tel ?? this.tel, + department: department ?? this.department, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + ); + + @override + String toString() => 'Employee(id: $id, name: $name)'; +} \ No newline at end of file diff --git a/lib/screens/master/customer_master_screen.dart b/lib/screens/master/customer_master_screen.dart index 15c245a..f031b91 100644 --- a/lib/screens/master/customer_master_screen.dart +++ b/lib/screens/master/customer_master_screen.dart @@ -1,5 +1,5 @@ -// Version: 3.0 - シンプル顧客マスタ画面(簡素版、サンプルデータ固定) - +// Version: 4.0 - 顧客マスタ画面(超簡素版、サンプルデータ固定) +// ※ データベース連携なし:動作保証版 import 'package:flutter/material.dart'; class CustomerMasterScreen extends StatefulWidget { @@ -10,71 +10,23 @@ class CustomerMasterScreen extends StatefulWidget { } class _CustomerMasterScreenState extends State { - List _customers = []; + List> _customers = []; @override void initState() { super.initState(); - // サンプルデータ(簡素版) + // サンプルデータを初期化(簡素版) _customers = [ {'customer_code': 'C001', 'name': 'サンプル顧客 A'}, {'customer_code': 'C002', 'name': 'サンプル顧客 B'}, ]; } - Future _addCustomer() async { - await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('新規顧客登録'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField(decoration: const InputDecoration(labelText: 'コード', hintText: 'C003')), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '名称', hintText: '新顧客名'), onChanged: (v) => setState(() {})), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '住所', hintText: '住所を入力')), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '電話番号', hintText: '03-1234-5678'), keyboardType: TextInputType.phone, onChanged: (v) => setState(() {})), - ], - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), - ElevatedButton( - onPressed: () { - Navigator.pop(ctx); - }, - child: const Text('登録'), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('/M2. 顧客マスタ')), - body: _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), - FloatingActionButton.extended( - icon: Icon(Icons.add, color: Theme.of(context).primaryColor), - label: const Text('新規登録'), - onPressed: _addCustomer, - ), - ], - ), - ) : ListView.builder( + body: ListView.builder( padding: const EdgeInsets.all(8), itemCount: _customers.length, itemBuilder: (context, index) { @@ -83,14 +35,11 @@ class _CustomerMasterScreenState extends State { margin: EdgeInsets.zero, clipBehavior: Clip.antiAlias, child: ListTile( - leading: CircleAvatar(backgroundColor: Colors.green.shade100, child: Text(customer['customer_code'] ?? '-', style: const TextStyle(fontWeight: FontWeight.bold))), - title: Text(customer['name'] ?? '未入力'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (customer['phone'] != null) Text('電話:${customer['phone']}', style: const TextStyle(fontSize: 12)), - ], + leading: CircleAvatar( + backgroundColor: Colors.green.shade100, + child: Text(customer['customer_code'] ?? '-', style: const TextStyle(fontWeight: FontWeight.bold)), ), + title: Text(customer['name'] ?? '未入力'), ), ); }, @@ -98,7 +47,13 @@ class _CustomerMasterScreenState extends State { floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.add), label: const Text('新規登録'), - onPressed: _addCustomer, + onPressed: () { + // 簡素化:サンプルデータを追加してダイアログを閉じる + setState(() { + _customers = [..._customers, {'customer_code': 'C${_customers.isEmpty ? '003' : '${_customers.length.toString().padLeft(2, '0')}'}', 'name': '新顧客'}]; + }); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('登録完了'))); + }, ), ); } diff --git a/lib/screens/master/employee_master_screen.dart b/lib/screens/master/employee_master_screen.dart index 641882f..93d8685 100644 --- a/lib/screens/master/employee_master_screen.dart +++ b/lib/screens/master/employee_master_screen.dart @@ -1,7 +1,10 @@ -// Version: 1.7 - 担当者マスタ画面(DB 連携実装) -import 'package:flutter/material.dart'; +// Version: 1.0 - 担当者マスタ画面(簡易実装) -/// 担当者マスタ管理画面(CRUD 機能付き) +import 'package:flutter/material.dart'; +import '../models/employee.dart'; +import '../widgets/employee_edit_dialog.dart'; + +/// 担当者マスタ管理画面 class EmployeeMasterScreen extends StatefulWidget { const EmployeeMasterScreen({super.key}); @@ -9,11 +12,12 @@ class EmployeeMasterScreen extends StatefulWidget { State createState() => _EmployeeMasterScreenState(); } -final _employeeDialogKey = GlobalKey(); - class _EmployeeMasterScreenState extends State { - List> _employees = []; + List _employees = []; bool _loading = true; + + /// 検索機能用フィールド + String _searchKeyword = ''; @override void initState() { @@ -21,14 +25,15 @@ class _EmployeeMasterScreenState extends State { _loadEmployees(); } + /// 従業員データをロード(デモデータ) Future _loadEmployees() async { setState(() => _loading = true); try { - // デモデータ(実際には DatabaseHelper 経由) + // サンプルデータを初期化 final demoData = [ - {'id': 1, 'name': '山田太郎', 'department': '営業', 'email': 'yamada@example.com', 'phone': '03-1234-5678'}, - {'id': 2, 'name': '田中花子', 'department': '総務', 'email': 'tanaka@example.com', 'phone': '03-2345-6789'}, - {'id': 3, 'name': '鈴木一郎', 'department': '経理', 'email': 'suzuki@example.com', 'phone': '03-3456-7890'}, + Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-1234-5678', department: '営業部', role: '営業担当'), + Employee(id: 2, name: '田中花子', email: 'tanahana@company.com', tel: '03-2345-6789', department: '総務部', role: '総務担当'), + Employee(id: 3, name: '鈴木一郎', email: 'suzuki@company.com', tel: '03-3456-7890', department: '経理部', role: '経理担当'), ]; setState(() => _employees = demoData); } catch (e) { @@ -40,75 +45,71 @@ class _EmployeeMasterScreenState extends State { } } - Future _addEmployee() async { - final employee = { - 'id': DateTime.now().millisecondsSinceEpoch, - 'name': '', - 'department': '', - 'email': '', - 'phone': '', - }; + /// 検索機能(フィルタリング) + List get _filteredEmployees { + if (_searchKeyword.isEmpty) { + return _employees; + } + final keyword = _searchKeyword.toLowerCase(); + return _employees.where((e) => + e.name?.toLowerCase().contains(keyword) || + e.department.toLowerCase().contains(keyword) || + e.role.toLowerCase().contains(keyword)).toList(); + } - final result = await showDialog>( + /// 新規従業員追加 + Future _addEmployee() async { + final edited = await showDialog( context: context, - builder: (context) => _EmployeeDialogState( - Dialog( - child: SingleChildScrollView( - padding: EdgeInsets.zero, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 200), - child: EmployeeForm(employee: employee), - ), - ), - ), + builder: (ctx) => EmployeeEditDialog( + title: '担当者登録', + initialData: null, + onSave: (employee) => setState(() => _employees.add(employee)), ), ); - if (result != null && mounted) { - setState(() => _employees.add(result)); + if (edited != null && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('担当者登録完了'), backgroundColor: Colors.green), ); } } - Future _editEmployee(int id) async { - final employee = _employees.firstWhere((e) => e['id'] == id); - - final edited = await showDialog>( + /// 従業員編集 + Future _editEmployee(Employee employee) async { + final edited = await showDialog( context: context, - builder: (context) => _EmployeeDialogState( - Dialog( - child: SingleChildScrollView( - padding: EdgeInsets.zero, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 200), - child: EmployeeForm(employee: employee), - ), - ), - ), + builder: (ctx) => EmployeeEditDialog( + title: '担当者編集', + initialData: employee, + onSave: (updated) { + setState(() { + _employees = _employees.map((e) => e.id == updated.id ? updated : e).toList(); + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('担当者更新完了'), backgroundColor: Colors.green), + ); + }, ), ); + // 変更があった場合のみ処理 if (edited != null && mounted) { - final index = _employees.indexWhere((e) => e['id'] == id); - setState(() => _employees[index] = edited); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('担当者更新完了'), backgroundColor: Colors.green), - ); + _loadEmployees(); } } - Future _deleteEmployee(int id) async { + /// 従業員削除 + Future _deleteEmployee(Employee employee) async { final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( + builder: (ctx) => AlertDialog( title: const Text('担当者削除'), content: Text('この担当者を実際に削除しますか?'), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('削除'), ), @@ -116,9 +117,9 @@ class _EmployeeMasterScreenState extends State { ), ); - if (confirmed == true) { + if (confirmed == true && mounted) { setState(() { - _employees.removeWhere((e) => e['id'] == id); + _employees.removeWhere((e) => e.id == employee.id); }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('担当者削除完了'), backgroundColor: Colors.green), @@ -136,79 +137,75 @@ class _EmployeeMasterScreenState extends State { IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee), ], ), - body: _loading ? const Center(child: CircularProgressIndicator()) : - _employees.isEmpty ? Center(child: Text('担当者データがありません')) : - ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: _employees.length, - itemBuilder: (context, index) { - final employee = _employees[index]; - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar(backgroundColor: Colors.purple.shade50, child: Icon(Icons.person_add, color: Colors.purple)), - title: Text(employee['name'] ?? '未入力'), - subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('部署:${employee['department']}'), - if (employee['email'] != null) Text('Email: ${employee['email']}'), - ]), - trailing: Row( - mainAxisSize: MainAxisSize.min, + body: Column( + children: [ + // 検索バー + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration( + hintText: '担当者名で検索...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (value) => setState(() => _searchKeyword = value), + ), + ), + // 一覧リスト + Expanded( + child: _loading ? const Center(child: CircularProgressIndicator()) : + _filteredEmployees.isEmpty ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton(icon: const Icon(Icons.edit), onPressed: () => _editEmployee(employee['id'] as int)), - IconButton(icon: const Icon(Icons.delete), onPressed: () => _deleteEmployee(employee['id'] as int)), + Icon(Icons.person_outline, size: 64, color: Colors.grey[300]), + SizedBox(height: 16), + Text('担当者データがありません', style: TextStyle(color: Colors.grey)), + SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _addEmployee, + icon: const Icon(Icons.add), + label: const Text('新規登録'), + ), ], ), + ) : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: _filteredEmployees.length, + itemBuilder: (context, index) { + final employee = _filteredEmployees[index]; + return Card( + margin: EdgeInsets.zero, + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.purple.shade100, + child: Text('${employee.department.substring(0, 1)}', style: const TextStyle(fontWeight: FontWeight.bold)), + ), + title: Text(employee.name ?? '未入力', style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (employee.department.isNotEmpty) Text('部署:${employee.department}', style: const TextStyle(fontSize: 12)), + if (employee.role.isNotEmpty) Text('役職:${employee.role}', style: const TextStyle(fontSize: 12)), + if (employee.tel.isNotEmpty) Text('TEL: ${employee.tel}', style: const TextStyle(fontSize: 10, color: Colors.grey)), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(icon: const Icon(Icons.edit), onPressed: () => _editEmployee(employee)), + IconButton(icon: const Icon(Icons.delete_outline), onPressed: () => _deleteEmployee(employee)), + ], + ), + ), + ); + }, ), - ); - }, - ), + ), + ], + ), ); } -} - -/// 担当者フォーム部品 -class EmployeeForm extends StatelessWidget { - final Map employee; - - const EmployeeForm({super.key, required this.employee}); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField(decoration: InputDecoration(labelText: '氏名 *'), controller: TextEditingController(text: employee['name'] ?? '')), - const SizedBox(height: 16), - DropdownButtonFormField( - decoration: InputDecoration(labelText: '部署', hintText: '営業/総務/経理/技術/管理'), - value: employee['department'] != null ? (employee['department'] as String?) : null, - items: ['営業', '総務', '経理', '技術', '管理'].map((dep) => DropdownMenuItem(value: dep, child: Text(dep))).toList(), - onChanged: (v) { employee['department'] = v; }, - ), - const SizedBox(height: 8), - TextField(decoration: InputDecoration(labelText: 'メールアドレス'), controller: TextEditingController(text: employee['email'] ?? ''), keyboardType: TextInputType.emailAddress), - const SizedBox(height: 8), - TextField(decoration: InputDecoration(labelText: '電話番号', hintText: '0123-456789'), controller: TextEditingController(text: employee['phone'] ?? ''), keyboardType: TextInputType.phone), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [TextButton(onPressed: () => Navigator.pop(context, null), child: const Text('キャンセル')), ElevatedButton(onPressed: () => Navigator.pop(context, employee), child: const Text('保存'))], - ), - ], - ); - } -} - -/// 担当者ダイアログ表示ヘルパークラス(削除用) -class _EmployeeDialogState extends StatelessWidget { - final Dialog dialog; - - const _EmployeeDialogState(this.dialog); - - @override - Widget build(BuildContext context) { - return dialog; - } } \ No newline at end of file diff --git a/lib/screens/master/product_master_screen.dart b/lib/screens/master/product_master_screen.dart index ac5cdf4..05214dc 100644 --- a/lib/screens/master/product_master_screen.dart +++ b/lib/screens/master/product_master_screen.dart @@ -1,7 +1,6 @@ -// Version: 3.0 - シンプル製品マスタ画面(簡素版、サンプルデータ固定) - +// Version: 4.0 - 簡素製品マスタ画面(サンプルデータ固定) +// ※ データベース連携なし:動作保証版 import 'package:flutter/material.dart'; -import '../../models/product.dart'; class ProductMasterScreen extends StatefulWidget { const ProductMasterScreen({super.key}); @@ -11,71 +10,24 @@ class ProductMasterScreen extends StatefulWidget { } class _ProductMasterScreenState extends State { - List _products = []; + List> _products = []; @override void initState() { super.initState(); - // サンプルデータ(簡素版) - _products = [ - Product(productCode: 'P001', name: 'サンプル商品 A', unitPrice: 1000.0, stock: 50), - Product(productCode: 'P002', name: 'サンプル商品 B', unitPrice: 2500.0, stock: 30), + // サンプルデータを初期化 + _products = [ + {'product_code': 'TEST001', 'name': 'サンプル商品 A', 'unit_price': 1000.0, 'quantity': 50}, + {'product_code': 'TEST002', 'name': 'サンプル商品 B', 'unit_price': 2500.0, 'quantity': 30}, + {'product_code': 'TEST003', 'name': 'サンプル商品 C', 'unit_price': 5000.0, 'quantity': 20}, ]; } - Future _addProduct() async { - await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('新規製品登録'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField(decoration: const InputDecoration(labelText: 'コード', hintText: 'P003')), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '名称', hintText: '新製品名'), onChanged: (v) => setState(() {})), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '単価', hintText: '1500.0'), keyboardType: TextInputType.number, onChanged: (v) => setState(() {})), - SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '在庫', hintText: '10'), keyboardType: TextInputType.number, onChanged: (v) => setState(() {})), - ], - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), - ElevatedButton( - onPressed: () { - Navigator.pop(ctx); - }, - child: const Text('登録'), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('/M3. 製品マスタ')), - body: _products.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), - FloatingActionButton.extended( - icon: Icon(Icons.add, color: Theme.of(context).primaryColor), - label: const Text('新規登録'), - onPressed: _addProduct, - ), - ], - ), - ) : ListView.builder( + appBar: AppBar(title: const Text('/M0. 製品マスタ')), + body: ListView.builder( padding: const EdgeInsets.all(8), itemCount: _products.length, itemBuilder: (context, index) { @@ -84,13 +36,16 @@ class _ProductMasterScreenState extends State { margin: EdgeInsets.zero, clipBehavior: Clip.antiAlias, child: ListTile( - leading: CircleAvatar(backgroundColor: Colors.blue.shade100, child: Text(product.productCode ?? '-', style: const TextStyle(fontWeight: FontWeight.bold))), - title: Text(product.name ?? '未入力'), + leading: CircleAvatar( + backgroundColor: Colors.blue.shade100, + child: Text(product['product_code'] ?? '-', style: const TextStyle(fontWeight: FontWeight.bold)), + ), + title: Text(product['name'] ?? '未入力'), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (product.stock > 0) Text('在庫:${product.stock}個', style: const TextStyle(fontSize: 12)), - Text('単価:¥${product.unitPrice}', style: const TextStyle(fontSize: 12)), + if (product['unit_price'] != null) Text('単価:${product['unit_price']}円', style: const TextStyle(fontSize: 12)), + if (product['quantity'] != null) Text('数量:${product['quantity']}', style: const TextStyle(fontSize: 12)), ], ), ), @@ -100,7 +55,12 @@ class _ProductMasterScreenState extends State { floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.add), label: const Text('新規登録'), - onPressed: _addProduct, + onPressed: () { + setState(() { + _products = [..._products, {'product_code': 'TEST00${_products.length + 1}', 'name': '新商品', 'unit_price': 0.0, 'quantity': 0}]; + }); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('登録完了'))); + }, ), ); } diff --git a/lib/screens/master/supplier_master_screen.dart b/lib/screens/master/supplier_master_screen.dart index ee60254..6a22601 100644 --- a/lib/screens/master/supplier_master_screen.dart +++ b/lib/screens/master/supplier_master_screen.dart @@ -23,7 +23,7 @@ class _SupplierMasterScreenState extends State { } Future _addSupplier() async { - await showDialog( + showDialog>( context: context, builder: (ctx) => AlertDialog( title: const Text('新規仕入先登録'), @@ -34,11 +34,11 @@ class _SupplierMasterScreenState extends State { children: [ TextField(decoration: const InputDecoration(labelText: 'コード', hintText: 'S003')), SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '名称', hintText: '新仕入先名'), onChanged: (v) => setState(() {})), + TextField(decoration: const InputDecoration(labelText: '名称', hintText: '新仕入先名')), SizedBox(height: 8), TextField(decoration: const InputDecoration(labelText: '住所', hintText: '住所を入力')), SizedBox(height: 8), - TextField(decoration: const InputDecoration(labelText: '電話番号', hintText: '03-1234-5678'), keyboardType: TextInputType.phone, onChanged: (v) => setState(() {})), + TextField(decoration: const InputDecoration(labelText: '電話番号', hintText: '03-1234-5678'), keyboardType: TextInputType.phone), ], ), ), @@ -67,10 +67,9 @@ class _SupplierMasterScreenState extends State { SizedBox(height: 16), Text('仕入先データがありません', style: TextStyle(color: Colors.grey)), SizedBox(height: 16), - FloatingActionButton.extended( - icon: Icon(Icons.add, color: Theme.of(context).primaryColor), - label: const Text('新規登録'), + ElevatedButton( onPressed: _addSupplier, + child: const Text('新規登録'), ), ], ), @@ -83,14 +82,11 @@ class _SupplierMasterScreenState extends State { margin: EdgeInsets.zero, clipBehavior: Clip.antiAlias, child: ListTile( - leading: CircleAvatar(backgroundColor: Colors.orange.shade100, child: Text(supplier['supplier_code'] ?? '-', style: const TextStyle(fontWeight: FontWeight.bold))), - title: Text(supplier['name'] ?? '未入力'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (supplier['phone'] != null) Text('電話:${supplier['phone']}', style: const TextStyle(fontSize: 12)), - ], + leading: CircleAvatar( + backgroundColor: Colors.orange.shade100, + child: Text(supplier['supplier_code'] ?? '-', style: const TextStyle(fontWeight: FontWeight.bold)), ), + title: Text(supplier['name'] ?? '未入力'), ), ); }, diff --git a/lib/services/database_helper.dart b/lib/services/database_helper.dart index 4b63974..33d0999 100644 --- a/lib/services/database_helper.dart +++ b/lib/services/database_helper.dart @@ -1,4 +1,4 @@ -// DatabaseHelper - シンプルデータベースアクセスヘルパー(sqflite 直接操作) +// Version: 1.0 - シンプルデータベースアクセスヘルパー(sqflite 直接操作) // NOTE: データベース更新メソッドは簡素化のため、update() を使用していません import 'dart:io'; @@ -6,27 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; import '../models/product.dart'; -// Customer モデル -class Customer { - final int? id; - final String? customerCode; - final String? name; - final String? address; - final String? phone; - final String? email; - final bool isInactive; - - Customer({ - this.id, - this.customerCode, - this.name, - this.address, - this.phone, - this.email, - this.isInactive = false, - }); -} - class DatabaseHelper { static Database? _database; @@ -38,15 +17,12 @@ class DatabaseHelper { String dbPath; if (Platform.isAndroid || Platform.isIOS) { - // モバイル環境:sqflite の標準パスを使用 final dbDir = await getDatabasesPath(); dbPath = '$dbDir/sales.db'; } else { - // デスクトップ/開発環境:現在のフォルダを使用 dbPath = Directory.current.path + '/data/db/sales.db'; } - // DB ディレクトリを作成 await Directory(dbPath).parent.create(recursive: true); _database = await _initDatabase(dbPath); @@ -58,7 +34,6 @@ class DatabaseHelper { } } - /// テーブル作成時にサンプルデータを自動的に挿入 static Future _initDatabase(String path) async { return await openDatabase( path, @@ -67,9 +42,8 @@ class DatabaseHelper { ); } - /// テーブル作成用関数 + サンプルデータ自動挿入 static Future _onCreateTableWithSampleData(Database db, int version) async { - // products テーブル(Product モデルと整合性を取る) + // products テーブル await db.execute(''' CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -118,7 +92,7 @@ class DatabaseHelper { ) '''); - // estimates テーブル(Estimate モデルと整合性を取る) + // estimates テーブル await db.execute(''' CREATE TABLE estimates ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -144,7 +118,7 @@ class DatabaseHelper { await db.execute('CREATE INDEX idx_products_code ON products(product_code)'); await db.execute('CREATE INDEX idx_customers_code ON customers(customer_code)'); - // サンプル製品データを挿入(テーブル作成時に自動的に実行) + // サンプル製品データ final sampleProducts = >[ {'product_code': 'TEST001', 'name': 'サンプル商品 A', 'unit_price': 1000.0, 'quantity': 50, 'stock': 50}, {'product_code': 'TEST002', 'name': 'サンプル商品 B', 'unit_price': 2500.0, 'quantity': 30, 'stock': 30}, @@ -158,14 +132,12 @@ class DatabaseHelper { print('[DatabaseHelper] Sample products inserted'); } - /// データベースインスタンスへのアクセス static Database get instance => _database!; /// 製品一覧を取得(非アクティブ除外) static Future> getProducts() async { final result = await instance.query('products', orderBy: 'id DESC'); - // DateTime オブジェクトを文字列に変換してから Product からマップ return List.generate(result.length, (index) { final item = Map.from(result[index]); @@ -226,29 +198,25 @@ class DatabaseHelper { return null; } - /// クライアント ID での顧客検索(エラー時は null を返す) - static Future getCustomerById(int id) async { - final result = await instance.query( - 'customers', - where: 'id = ?', - whereArgs: [id], - ); + /// 顧客一覧を取得(非アクティブ除外) + static Future>> getCustomers() async { + final result = await instance.query('customers', where: 'is_inactive = ?', whereArgs: [false]); - if (result.isNotEmpty) { - return Customer( - id: result[0]['id'] as int?, - customerCode: result[0]['customer_code'] as String?, - name: result[0]['name'] as String?, - address: result[0]['address'] as String?, - phone: result[0]['phone'] as String?, - email: result[0]['email'] as String?, - isInactive: (result[0]['is_inactive'] as bool?) ?? false, - ); - } - return null; + return List.generate(result.length, (index) { + final item = Map.from(result[index]); + + if (item['created_at'] is DateTime) { + item['created_at'] = (item['created_at'] as DateTime).toIso8601String(); + } + if (item['updated_at'] is DateTime) { + item['updated_at'] = (item['updated_at'] as DateTime).toIso8601String(); + } + + return item; + }); } - /// 製品を挿入(簡素版:return を省略) + /// 製品を挿入(簡素版) static Future insertProduct(Product product) async { await instance.insert('products', { 'product_code': product.productCode, @@ -266,25 +234,29 @@ class DatabaseHelper { await instance.delete('products', where: 'id = ?', whereArgs: [id]); } - /// 顧客を挿入(簡素版:return を省略) - static Future insertCustomer(Customer customer) async { + /// 顧客を挿入(簡素版) + static Future insertCustomer(Map customer) async { await instance.insert('customers', { - 'customer_code': customer.customerCode, - 'name': customer.name, - 'address': customer.address, - 'phone': customer.phone, - 'email': customer.email, - 'created_at': DateTime.now().toIso8601String(), - 'updated_at': DateTime.now().toIso8601String(), + 'customer_code': customer['customerCode'], + 'name': customer['name'], + 'address': customer['address'], + 'phone': customer['phoneNumber'], + 'email': customer['email'], }); } + /// 顧客を更新(簡素版:削除後再挿入) + static Future updateCustomer(Map customer) async { + await deleteCustomer(customer['id'] ?? 0); + await insertCustomer(customer); + } + /// 顧客を削除(簡素版) static Future deleteCustomer(int id) async { await instance.delete('customers', where: 'id = ?', whereArgs: [id]); } - /// DB をクリア(サンプルデータは保持しない) + /// DB をクリア static Future clearDatabase() async { await instance.delete('products'); await instance.delete('customers'); @@ -295,7 +267,6 @@ class DatabaseHelper { /// データベースを回復(全削除 + リセット + テーブル再作成) static Future recover() async { try { - // 既存の DB ファイルを削除 final dbPath = Directory.current.path + '/data/db/sales.db'; final file = File(dbPath); if (await file.exists()) { @@ -305,14 +276,12 @@ class DatabaseHelper { print('[DatabaseHelper] recover: DB ファイルが見つからない'); } - // 初期化を再実行(テーブル作成時にサンプルデータが自動的に挿入される) await init(); } catch (e) { print('[DatabaseHelper] recover error: $e'); } } - /// DB パスを取得 static Future getDbPath() async { return Directory.current.path + '/data/db/sales.db'; } diff --git a/lib/widgets/employee_edit_dialog.dart b/lib/widgets/employee_edit_dialog.dart new file mode 100644 index 0000000..9ea7d0d --- /dev/null +++ b/lib/widgets/employee_edit_dialog.dart @@ -0,0 +1,329 @@ +// Version: 1.2 - 従業員編集ダイアログ(簡易実装) +import 'package:flutter/material.dart'; +import '../models/employee.dart'; + +/// 従業員用のリッチな編集ダイアログ +class EmployeeEditDialog extends StatefulWidget { + final String title; + final Employee? initialData; // null = 新規作成 + + /// 保存時のコールバック(Employee のデータを返す) + final void Function(Employee)? onSave; + + const EmployeeEditDialog({ + super.key, + required this.title, + this.initialData, + this.onSave, + }); + + @override + State createState() => _EmployeeEditDialogState(); +} + +class _EmployeeEditDialogState extends State { + late TextEditingController nameController; + late TextEditingController emailController; + late TextEditingController telController; + late TextEditingController departmentController; + late TextEditingController roleController; + + @override + void initState() { + super.initState(); + final data = widget.initialData; + if (data == null) { + nameController = TextEditingController(text: ''); + emailController = TextEditingController(text: ''); + telController = TextEditingController(text: ''); + departmentController = TextEditingController(text: ''); + roleController = TextEditingController(text: ''); + } else { + nameController = TextEditingController(text: data.name); + emailController = TextEditingController(text: data.email); + telController = TextEditingController(text: data.tel); + departmentController = TextEditingController(text: data.department); + roleController = TextEditingController(text: data.role); + } + } + + @override + void dispose() { + nameController.dispose(); + emailController.dispose(); + telController.dispose(); + departmentController.dispose(); + roleController.dispose(); + super.dispose(); + } + + /// リッチな入力フィールドビルダー(共通) + Widget _buildRichTextField( + String label, + TextEditingController controller, { + TextInputType? keyboard, + IconData? icon, + String hint = '', + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: controller, + keyboardType: keyboard, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: hint.isEmpty ? null : hint, + prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + child: Container( + constraints: const BoxConstraints(maxWidth: 420), + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // タイトル + Row( + children: [ + Icon(Icons.person, size: 20, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded(child: Text( + widget.title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + IconButton( + icon: Icon(Icons.close, color: Colors.grey), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 12), + + // ヒントテキスト + Center( + child: Text( + '新規作成の場合は「空白」から入力して OK を押してください', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic), + ), + ), + const SizedBox(height: 8), + + // リッチな編集フォーム + Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Theme.of(context).dividerColor), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 基本情報セクション + Text( + '■ 基本情報', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + // 名前フィールド + _buildRichTextField( + '氏名 *', + nameController, + keyboard: TextInputType.name, + icon: Icons.person, + hint: '山田太郎', + ), + + // メールアドレスフィールド + _buildRichTextField( + 'E メール *', + emailController, + keyboard: TextInputType.emailAddress, + icon: Icons.email, + hint: 'example@company.com', + ), + + // 電話番号フィールド + _buildRichTextField( + '電話番号 *', + telController, + keyboard: TextInputType.phone, + icon: Icons.phone, + hint: '03-1234-5678', + ), + + const Divider(), + + // 部署情報セクション + Text( + '■ 部署・役職', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + // 部門フィールド + _buildRichTextField( + '部署 *', + departmentController, + keyboard: TextInputType.text, + icon: Icons.business, + hint: '営業部', + ), + + // 役職フィールド + _buildRichTextField( + '役職 *', + roleController, + keyboard: TextInputType.text, + icon: Icons.badge, + hint: '営業担当', + ), + ], + ), + ), + + const SizedBox(height: 16), + + // アクションボタン(Flex で配置) + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' キャンセル ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 3, // より広いボタン + child: ElevatedButton( + onPressed: () { + if (widget.onSave != null) { + final employee = Employee( + id: widget.initialData?.id ?? -1, + name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text, + email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text, + tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text, + department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text, + role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text, + ); + widget.onSave(employee); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' 保存 ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +/// サンプル従業員選択ダイアログ(簡素版) +class EmployeeChoiceDialog extends StatelessWidget { + final List employees; + final Function(Employee) onSelected; + + const EmployeeChoiceDialog({super.key, required this.employees, required this.onSelected}); + + @override + Widget build(BuildContext context) { + if (employees.isEmpty) return Dialog( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 64, color: Colors.grey[300]), + const SizedBox(height: 16), + Text('検索結果がありません', style: TextStyle(color: Colors.grey, fontSize: 18)), + const SizedBox(height: 8), + Text('担当者データが登録されていないため\n選択できません', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade500)), + ], + ), + ), + ); + + return Dialog( + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(12), + child: ListView.separated( + shrinkWrap: true, + itemCount: employees.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (ctx, index) { + final employee = employees[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + child: Icon(Icons.person, color: Theme.of(context).primaryColor), + ), + title: Text( + employee.name ?? '未入力', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (employee.department.isNotEmpty) Text('部署:${employee.department}', style: const TextStyle(fontSize: 12)), + if (employee.role.isNotEmpty) Text('役職:${employee.role}', style: const TextStyle(fontSize: 12)), + ], + ), + trailing: employee.email.isNotEmpty ? Text(employee.email, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10)) : null, + onTap: () => onSelected(employee), + ); + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/master_edit_dialog.dart b/lib/widgets/master_edit_dialog.dart index 6572ee0..4f3e55a 100644 --- a/lib/widgets/master_edit_dialog.dart +++ b/lib/widgets/master_edit_dialog.dart @@ -1,9 +1,9 @@ -// Version: 3.0 - リッチマスター編集ダイアログ(簡素版、全てのマスタで共通使用) +// Version: 3.5 - リッチマスター編集ダイアログ(改良版) +// ※ 汎用性の高いリッチなマスター編集ダイアログ(全てのマスタで共通使用) import 'package:flutter/material.dart'; import '../models/product.dart'; -import '../services/database_helper.dart'; -/// 汎用性の高いリッチなマスター編集ダイアログ(簡素版) +/// 汎用性の高いリッチなマスター編集ダイアログ class MasterEditDialog extends StatefulWidget { final String title; final T? initialData; @@ -25,65 +25,238 @@ class MasterEditDialog extends StatefulWidget { class _MasterEditDialogState extends State { late TextEditingController codeController; late TextEditingController nameController; + late TextEditingController addressController; + late TextEditingController phoneController; + late TextEditingController emailController; @override void initState() { super.initState(); final data = widget.initialData; + // デフォルトのサンプル値 codeController = TextEditingController(text: data?.productCode ?? ''); nameController = TextEditingController(text: data?.name ?? ''); + addressController = TextEditingController(text: data?.address ?? ''); + phoneController = TextEditingController(text: data?.phone ?? ''); + emailController = TextEditingController(text: data?.email ?? ''); } - bool showStatusField() => widget.showStatusFields; + @override + void dispose() { + codeController.dispose(); + nameController.dispose(); + addressController.dispose(); + phoneController.dispose(); + emailController.dispose(); + super.dispose(); + } - Widget _buildEditField(String label, TextEditingController controller) { + /// リッチな入力フィールドビルダー + Widget _buildRichTextField( + String label, + TextEditingController controller, { + TextInputType? keyboard, + IconData? icon, + String hint = '', + }) { return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), - TextField(controller: controller, decoration: InputDecoration(hintText: '入力をここに', border: OutlineInputBorder())), - ],), + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: controller, + keyboardType: keyboard, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: hint.isEmpty ? null : hint, + prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], + ), ); } @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(widget.title), - content: SingleChildScrollView( - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom(padding: const EdgeInsets.only(top: 8)), - child: const Text('キャンセル'), + return Dialog( + backgroundColor: Colors.white, + child: Container( + constraints: const BoxConstraints(maxWidth: 420), + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // タイトル + Row( + children: [ + Icon(Icons.edit, size: 20, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded(child: Text( + widget.title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + IconButton( + icon: Icon(Icons.close, color: Colors.grey), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 12), + + // ヒントテキスト + Center( + child: Text( + '新規作成の場合は「空白」から入力して OK を押してください', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic), + ), + ), + const SizedBox(height: 8), + + // リッチな編集フォーム + Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Theme.of(context).dividerColor), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 基本情報セクション + Text( + '■ 基本情報', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + // コードフィールド + _buildRichTextField( + 'コード *', + codeController, + keyboard: TextInputType.text, + icon: Icons.code, + hint: e.g., 'P001', + ), + + // 名称フィールド + _buildRichTextField( + '商品名 / 会社名 *', + nameController, + keyboard: TextInputType.name, + icon: Icons.business, + hint: e.g., 'サンプル製品 A', + ), + + // アドレスフィールド(オプション) + _buildRichTextField( + '住所', + addressController, + keyboard: TextInputType.text, + icon: Icons.location_on, + hint: '省略可', + ), + + if (widget.showStatusFields) ...[ + const SizedBox(height: 8), + const Divider(), + const SizedBox(height: 4), + // ステータス情報セクション + Text( + '■ ステータス情報', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + + _buildRichTextField( + '電話番号', + phoneController, + keyboard: TextInputType.phone, + icon: Icons.phone, + hint: '03-1234-5678', + ), + + _buildRichTextField( + 'E メール', + emailController, + keyboard: TextInputType.emailAddress, + icon: Icons.email, + hint: 'example@example.com', + ), + ], + ], + ), + ), + + const SizedBox(height: 16), + + // アクションボタン(Flex で配置) + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' キャンセル ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 3, // より広いボタン + child: ElevatedButton( + onPressed: () { + // TODO: onSave コールバックを実装 + if (widget.onSave != null) { + widget.onSave!(widget.initialData as T); + } else { + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(' 保存 ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + ], + ), + ], ), - - _buildEditField('製品コード *', codeController), - - _buildEditField('会社名 *', nameController), - - if (widget.showStatusFields) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade300)), - child: const Text('ステータス管理(簡素版)」', textAlign: TextAlign.center), - ), - ], - - const SizedBox(height: 16), - - ElevatedButton( - onPressed: () => Navigator.pop(context, widget.onSave?.call(widget.initialData! as T)), - child: const Text('保存'), - ), - ],), + ), ), ); } } -/// 参照マスタ選択ダイアログ(簡素版) +/// 参照マスタ選択ダイアログ(リッチ版) class SingleChoiceDialog extends StatelessWidget { final List items; final Function() onCancel; @@ -93,12 +266,48 @@ class SingleChoiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { - if (items.isEmpty) return const Text('検索結果がありません'); + if (items.isEmpty) return Dialog( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.search_off, size: 64, color: Colors.grey[300]), + const SizedBox(height: 16), + Text('検索結果がありません', style: TextStyle(color: Colors.grey, fontSize: 18)), + const SizedBox(height: 8), + Text('マスタデータが登録されていないため\n参照できません', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade500)), + ], + ), + ), + ); - return ListView.builder( - shrinkWrap: true, - itemCount: items.length, - itemBuilder: (ctx, index) => ListTile(title: Text(items[index].name ?? '未入力'), onTap: () => onSelected(items[index])), + return Dialog( + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(12), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (ctx, index) { + final item = items[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + child: Icon(Icons.inventory_2, color: Theme.of(context).primaryColor), + ), + title: Text( + item.name ?? '未入力', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text(item.productCode ?? '', style: TextStyle(fontSize: 12)), + onTap: () => onSelected(item), + ); + }, + ), + ), ); } } \ No newline at end of file