diff --git a/@workspace/lib/pdf_templates/estimate_template.dart b/@workspace/lib/pdf_templates/estimate_template.dart new file mode 100644 index 0000000..a857b42 --- /dev/null +++ b/@workspace/lib/pdf_templates/estimate_template.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:printing/printing.dart'; + +/// 見積書 Widget(Printing で PDF 出力用) +class EstimateWidget extends StatelessWidget { + final String companyName; + final String companyAddress; + final String companyTel; + final String customerName; + final DateTime estimateDate; + final double totalAmount; + final List> items; + + const EstimateWidget({ + super.key, + required this.companyName, + required this.companyAddress, + required this.companyTel, + required this.customerName, + required this.estimateDate, + required this.totalAmount, + required this.items, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ヘッダー + Padding( + padding: const EdgeInsets.only(top: 48.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(companyName, style: const TextStyle(fontSize: 20)), + const SizedBox(height: 6), + Text(companyAddress, style: const TextStyle(fontSize: 10)), + Text(companyTel, style: const TextStyle(fontSize: 10)), + ], + ), + ), + const SizedBox(height: 6), + + // カスタマー情報(並列) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('見積書', style: TextStyle(fontSize: 16)), + const SizedBox(height: 4), + Text(customerName, style: const TextStyle(fontSize: 12)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text('日付:', style: TextStyle(fontSize: 12)), + const SizedBox(height: 2), + Text(DateFormat('yyyy/MM/dd').format(estimateDate), style: const TextStyle(fontSize: 12)), + ], + ), + ], + ), + const SizedBox(height: 6), + + // アイテムリスト(スクロール不可) + SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + children: [ + ...items.map((item) => Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${item['productName']} (${item['quantity']}個)', style: const TextStyle(fontSize: 10)), + Text('¥${item['totalAmount']}'.replaceAllMapped(RegExp(r'\d{1,3}(?=(\d{3})+(\$))'), (Match m) => '\${m[0]}'), style: const TextStyle(fontSize: 10)), + ], + ), + )), + ], + ), + ), + + // フッター(合計) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('合計', style: TextStyle(fontSize: 14)), + Text('¥${totalAmount}'.replaceAllMapped(RegExp(r'\d{1,3}(?=(\d{3})+(\$))'), (Match m) => '\${m[0]}'), style: const TextStyle(fontSize: 16)), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/@workspace/lib/widgets/master_edit_fields.dart b/@workspace/lib/widgets/master_edit_fields.dart new file mode 100644 index 0000000..438407e --- /dev/null +++ b/@workspace/lib/widgets/master_edit_fields.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; + +/// マスタ編集用の汎用テキストフィールドウィジェット +class MasterTextField extends StatelessWidget { + final String label; + final String? initialValue; + final String? hintText; + final VoidCallback? onTap; + + const MasterTextField({ + super.key, + required this.label, + this.initialValue, + this.hintText, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + TextFormField( + initialValue: initialValue, + decoration: InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + ), + onTap: onTap, + textInputAction: TextInputAction.done, + ), + ], + ), + ); + } +} + +/// マスタ編集用の汎用数値フィールドウィジェット +class MasterNumberField extends StatelessWidget { + final String label; + final double? initialValue; + final String? hintText; + final VoidCallback? onTap; + + const MasterNumberField({ + super.key, + required this.label, + this.initialValue, + this.hintText, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + TextFormField( + initialValue: initialValue?.toString(), + decoration: InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + ), + onTap: onTap, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + ), + ], + ), + ); + } +} + +/// ドロップダウンフィールドウィジェット +class MasterDropdownField extends StatelessWidget { + final String label; + final List options; + final String? selectedOption; + final VoidCallback? onTap; + + const MasterDropdownField({ + super.key, + required this.label, + required this.options, + this.selectedOption, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + DropdownButtonFormField( + value: selectedOption, + items: options.map((option) => DropdownMenuItem( + value: option, + child: Text(option), + )).toList(), + decoration: InputDecoration( + hintText: options.isEmpty ? null : options.first, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + ), + onTap: onTap, + isExpanded: true, + ), + ], + ), + ); + } +} + +/// テキストエリアウィジェット +class MasterTextArea extends StatelessWidget { + final String label; + final String? initialValue; + final String? hintText; + final VoidCallback? onTap; + + const MasterTextArea({ + super.key, + required this.label, + this.initialValue, + this.hintText, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + TextFormField( + initialValue: initialValue, + decoration: InputDecoration( + hintText: hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + ), + maxLines: 3, + onTap: onTap, + textInputAction: TextInputAction.newline, + ), + ], + ), + ); + } +} + +/// チェックボックスウィジェット +class MasterCheckBox extends StatelessWidget { + final String label; + final bool? initialValue; + final VoidCallback? onChanged; + + const MasterCheckBox({ + super.key, + required this.label, + this.initialValue, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + Expanded(child: Text(label)), + Checkbox( + value: initialValue, + onChanged: onChanged ?? (_ => null), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/README.md b/README.md index 85fd9fc..77d9707 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,4 @@ -# 販売アシスト 1 号「H-1Q」プロジェクト - Engineering Management +# 📦 Sales Assist - H-1Q (Flutter) -**開発コード**: **H-1Q(開発期間中)** -**最終更新日**: 2026/03/09 -**バージョン**: 1.7 (Sprint 4 完了 + 請求転換 UI 実装) ✅NEW - ---- - -## 📚 プロジェクトドキュメントと活用方法 - -### 📖 導入概要 - -この README は、プロジェクト管理に使用される工程管理ドキュメントへの入り口です。 - -- **`docs/project_plan.md`**: 全体の計画書(マイルストーン・スケジュール、開発コード:**H-1Q**) -- **`docs/short_term_plan.md`**: 短期計画(スプリントごとのタスクリスト、開発コード:**H-1Q**) -- **`docs/engineering_management.md`**: 工程管理プロセスのガイド -- **`docs/requirements.md`**: 機能要件定義書 - ---- - -## ✅ 実装完了セクション(Sprint 4: 2026/03/09-2026/03/23) - -### 📦 コア機能強化 - 完了済み ✅ - -| 機能 | ステータス | 詳細 | -|------|------|-|-| -| **見積入力機能** | ✅ 完了(簡素化対応) | DatabaseHelper 接続、エラーハンドリング完全化、Map データ保存方式へ簡素化 | -| **売上入力機能** | ✅ 完了 | JAN コード検索、合計金額計算、PDF 帳票出力対応(printing パッケージ) | -| **PDF 帳票出力** | ✅ 完了 | A5 サイズ・テンプレート設計完了、DocumentDirectory 保存ロジック実装済み | -| **売上データ保存 API** | ✅ 完了 | DatabaseHelper.insertSales の完全実装、顧客情報連携済み | -| **見積→請求転換 UI** | ✅ 完了(Sprint 5 移行) | estimate_screen.dart に転換ボタン追加、API で請求作成・状態更新ロジック実装 | - ---- - -### 🔄 Sprint 5: 請求機能・在庫管理(進行中)✅NEW - -| 機能 | ステータス | 詳細 | -|------|------|-|-| -| **見積→請求転換** | ✅ 実装済み | DB INSERT + status UPDATE、UI フィードバック追加 | -| **DocumentDirectory 自動保存** | ✅ 完了済み | sales_screen.dart の PDF 出力ロジックと連携中 | - -**担当**: Estimate チーム -**工期**: 2026/03/09(本日) -**優先度**: 🟢 High → **H-1Q-S5-M1 移行** ✅ - ---- - -### 📋 Sprint 4 タスク完了ログ - -- [x] DatabaseHelper.insertEstimate の完全なエラーハンドリング(重複チェック)→ Map データ方式へ簡素化対応 -- [x] `sales_screen.dart` の得意先選択機能実装 -- [x] 売上データ保存時の顧客情報連携 -- [x] PDF テンプレートバグ修正(行数計算・顧客名表示) -- [x] DocumentDirectory への自動保存ロジック実装 -- [x] **見積→請求転換 UI 実装** (2026/03/09) ✅NEW - -#### 🔄 Sprint 5 移行タスク(完了済み)✅ - -- [x] estimate_screen.dart に請求転換ボタン追加 -- [x] DatabaseHelper.insertInvoice API の重複チェック実装 -- [x] Estimate から Invoice へのデータ転換ロジック実装 -- [x] UI:転換完了通知 + 請求書画面遷移案内 - -**担当**: Estimate チーム -**工期**: 2026/03/09(本日完了) -**優先度**: 🟢 High → **H-1Q-S5-M1 移行** ✅ - ---- - -### 📝 実装対応履歴 - -#### Sprint 4 完了(2026/03/08) -- EstimateScreen の簡素化:Estimate モデル依存の排除(Map データ保存方式) -- 売上入力画面完全実装 + PDF テンプレートバグ修正 -- DocumentDirectory 自動保存ロジック実装 - -#### Sprint 5 移行(2026/03/09)✅NEW -- **見積→請求転換 UI**:estimate_screen.dart に転換ボタン追加 -- **API 強化**: DatabaseHelper.insertInvoice に重複チェック実装 -- **UI フィードバック**: 転換完了通知 + 請求書画面遷移案内 - -#### ビルド結果 (2026/03/09) -- app-release.apk (~48MB) -- DocumentDirectory: sales.pdf, estimate_YYYYMM.pdf - ---- - -## 🚧 Sprint 5: 請求機能・在庫管理(進行中)✅ - -### 📋 タスク定義(実装完了済み)✅ - -| タスク | ステータス | 詳細 | -|------|------|-|-| -| **見積→請求転換 UI** | ✅ **実装済み** | estimate_screen.dart に転換ボタン追加、DB INSERT + status UPDATE ロジック実装 | -| **DocumentDirectory 自動保存** | ✅ **完了済み** | sales_screen.dart の PDF 出力ロジックと連携中 | - -### 📅 Sprint 5 スケジュール(実装完了)✅NEW - -- **開始**: 2026/03/09 -- **完了**: 2026/03/09(S4-M4 完了)✅ -- **マイルストーン**: S4-M4 達成(請求機能 UI 実装完了) - -**担当**: Estimate チーム -**工期**: 2026/03/09(本日完了) -**優先度**: 🟢 High → **H-1Q-S5-M1 移行** ✅ - ---- - -## 🚧 Sprint 6: クラウド同期・在庫管理(計画段階) - -### 📋 タスク定義(予定) - -| タスク | ステータス | 詳細 | -|------|------|-|-| -| **見積→請求転換 UI** | ✅ **完了済み** | DB INSERT + status UPDATE、UI フィードバック実装 | -| **Inventory モデル** | ⚪ 未着手 | 在庫管理用のモデル定義と DatabaseHelper API | -| **PDF 領収書テンプレート** | ⚪ 計画段階 | 領収書のデザイン・レイアウト設計 | -| **Google 認証統合** | ⚪ 計画段階 | `google_sign_in` パッケージの導入検討 | - -### 📅 Sprint 6 スケジュール(見込み)→ H-1Q-S6 - -- **開始**: 2026/04/01 -- **完了**: 2026/04/15 -- **マイルストーン**: S6-M1(在庫管理 UI 実装)✅NEW → **H-1Q-S6 移行** - ---- - -## 🚧 進行中タスク - -| タスク | 進捗 | 担当者 | -|------|-|-|-| -| **見積→請求転換** | ✅ **完了** | Estimate チーム (3/09) | -| **DocumentDirectory 自動保存** | ✅ 完了済み | UI/UX チーム | -| **PDF 帳票出力ロジック(printing)** | ✅ 完了 | Sales チーム | -| **売上入力画面完全実装** | ✅ 完了 | Sales チーム (3/09) | - ---- - -## 📊 技術スタック - -- **Flutter**: UI フレームワーク (3.41.2) -- **SQLite**: ローカルデータベース(sqflite パッケージ) -- **printing**: PDF 帳票出力(flutter_pdf_generator 代替) -- **SharePlus**: PDF 領収書共有機能 -- **Intl**: 日付・通貨フォーマット - ---- - -## 📝 変更履歴 - -| 日付 | バージョン | 変更内容 | -|------|-|-|-| -| **2026/03/09** | **1.7** | **Sprint 4 完了 + 請求転換 UI 実装、DocumentDirectory 自動保存完了、CMO-01 → H-1Q に変更** ✅NEW | -| 2026/03/08 | 1.5 | Sprint 4 完了、M1 マイルストーン達成、見積簡素化対応 | -| 2026/03/08 | 1.4 | Sales Input + PDF Ready | -| 2026/03/08 | 1.3 | Sales Input + PDF Ready | - ---- - -**最終更新**: 2026/03/09 -**ビルド結果**: app-release.apk (~48MB) -**Sprint 5: 請求機能 UI 実装完了** ✅NEW -**開発コード**: **H-1Q(開発期間中、正式リリース後に販売アシスト 1 号へ変更)** - -**📌 注記**: 本プロジェクトの公式アプリ名は「販売アシスト 1 号」です。開発期間中は「H-1Q」として管理・参照してください。 \ No newline at end of file +**バージョン:** 1.5 +**コミット:** `13f7e \ No newline at end of file diff --git a/lib/screens/estimate_screen.dart b/lib/screens/estimate_screen.dart index 1af8883..e111805 100644 --- a/lib/screens/estimate_screen.dart +++ b/lib/screens/estimate_screen.dart @@ -248,7 +248,7 @@ class _EstimateScreenState extends State with SingleTickerProvid Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('見積書'), + title: const Text('/S1. 見積書'), actions: [ // 🔄 請求転換ボタン(Sprint 5: HIGH 優先度)✅実装済み IconButton( diff --git a/lib/screens/invoice_screen.dart b/lib/screens/invoice_screen.dart index c9af8c1..02c5048 100644 --- a/lib/screens/invoice_screen.dart +++ b/lib/screens/invoice_screen.dart @@ -53,7 +53,7 @@ class _InvoiceScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('請求書')), + appBar: AppBar(title: const Text('/S2. 請求書')), body: _selectedCustomer == null ? const Center(child: Text('得意先を選択してください')) : SingleChildScrollView( diff --git a/lib/screens/master/customer_master_screen.dart b/lib/screens/master/customer_master_screen.dart index cdcb61b..a285d1e 100644 --- a/lib/screens/master/customer_master_screen.dart +++ b/lib/screens/master/customer_master_screen.dart @@ -201,7 +201,7 @@ class _CustomerMasterScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('得意先マスタ'), + title: const Text('/M2. 得意先マスタ'), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers)], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : diff --git a/lib/screens/master/employee_master_screen.dart b/lib/screens/master/employee_master_screen.dart index 62d523d..641882f 100644 --- a/lib/screens/master/employee_master_screen.dart +++ b/lib/screens/master/employee_master_screen.dart @@ -130,7 +130,7 @@ class _EmployeeMasterScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('担当者マスタ'), + title: const Text('/M5. 担当者マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee), diff --git a/lib/screens/master/product_master_screen.dart b/lib/screens/master/product_master_screen.dart index ba02c8f..3f3daca 100644 --- a/lib/screens/master/product_master_screen.dart +++ b/lib/screens/master/product_master_screen.dart @@ -136,7 +136,7 @@ class _ProductMasterScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('商品マスタ'), + title: const Text('/M1. 商品マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,), IconButton(icon: const Icon(Icons.add), onPressed: _onAddPressed,), diff --git a/lib/screens/master/supplier_master_screen.dart b/lib/screens/master/supplier_master_screen.dart index c90e789..f5a3f88 100644 --- a/lib/screens/master/supplier_master_screen.dart +++ b/lib/screens/master/supplier_master_screen.dart @@ -111,7 +111,7 @@ class _SupplierMasterScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('仕入先マスタ'), + title: const Text('/M3. 仕入先マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers), IconButton(icon: const Icon(Icons.add), onPressed: _showAddDialog,), diff --git a/lib/screens/master/warehouse_master_screen.dart b/lib/screens/master/warehouse_master_screen.dart index 635dcd5..39c9805 100644 --- a/lib/screens/master/warehouse_master_screen.dart +++ b/lib/screens/master/warehouse_master_screen.dart @@ -132,7 +132,7 @@ class _WarehouseMasterScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('倉庫マスタ'), + title: const Text('/M4. 倉庫マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadWarehouses), IconButton(icon: const Icon(Icons.add), onPressed: _addWarehouse), diff --git a/lib/screens/order_screen.dart b/lib/screens/order_screen.dart index 525c65f..7715556 100644 --- a/lib/screens/order_screen.dart +++ b/lib/screens/order_screen.dart @@ -38,7 +38,7 @@ class _OrderScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('受注')), + appBar: AppBar(title: const Text('/S3. 発注入力')), body: _selectedCustomer == null ? const Center(child: Text('得意先を選択してください')) : SingleChildScrollView( diff --git a/lib/screens/sales_return_screen.dart b/lib/screens/sales_return_screen.dart index c8b4ed2..934a5e0 100644 --- a/lib/screens/sales_return_screen.dart +++ b/lib/screens/sales_return_screen.dart @@ -9,7 +9,7 @@ class SalesReturnScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('売上返品入力'), + title: const Text('/S5. 売上返品入力'), actions: [ IconButton( icon: const Icon(Icons.undo), diff --git a/lib/screens/sales_screen.dart b/lib/screens/sales_screen.dart index bff13be..8281999 100644 --- a/lib/screens/sales_screen.dart +++ b/lib/screens/sales_screen.dart @@ -140,7 +140,7 @@ class _SalesScreenState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('売上入力'), actions: [ + appBar: AppBar(title: const Text('/S4. 売上入力(レジ)'), actions: [ IconButton(icon: const Icon(Icons.save), onPressed: saveSalesData,), IconButton(icon: const Icon(Icons.share), onPressed: generateAndShareInvoice,), PopupMenuButton( diff --git a/lib/widgets/master_edit_fields.dart b/lib/widgets/master_edit_fields.dart index 6719453..6850e10 100644 --- a/lib/widgets/master_edit_fields.dart +++ b/lib/widgets/master_edit_fields.dart @@ -11,7 +11,6 @@ class MasterTextField extends StatelessWidget { final int maxLines; final TextInputAction textInputAction; final FormFieldValidator? validator; - // TextEditingController を直接使うため、onChanged は不要。nullable の形に定義する final void Function(String)? onChanged; const MasterTextField({ @@ -41,7 +40,7 @@ class MasterTextField extends StatelessWidget { obscureText: obscureText, maxLines: maxLines, textInputAction: textInputAction, - validator: (value) => onChanged?.call(value) ?? validator?.call(value), + validator: (value) => onChanged == null ? validator?.call(value) : 'Custom validation', onChanged: onChanged, ); } @@ -53,7 +52,6 @@ class MasterNumberField extends StatelessWidget { final TextEditingController controller; final String? hint; final FormFieldValidator? validator; - // Nullable の形で定義 final void Function(String)? onChanged; const MasterNumberField({ @@ -76,123 +74,52 @@ class MasterNumberField extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), keyboardType: TextInputType.number, - validator: (value) => onChanged?.call(value) ?? validator?.call(value), + validator: (value) => onChanged == null ? validator?.call(value) : 'Custom validation', onChanged: onChanged, ); } } -/// ドロップダウンフィールド -class MasterDropdownField extends StatelessWidget { +/// マスタ編集用の Checkbox +class MasterCheckboxField extends StatelessWidget { final String label; - final TextEditingController controller; - final T? initialSelectedValue; - final List dataSource; - final FormFieldValidator? validator; - // DropdownButtonFormField の onChanged は void Function(T)? を要求 - final void Function(T)? onChanged; + final bool value; + final ValueChanged? onChangedCallback; - const MasterDropdownField({ + const MasterCheckboxField({ super.key, required this.label, - required this.controller, - this.initialSelectedValue, - required this.dataSource, - this.validator, - this.onChanged, + required this.value, + this.onChangedCallback, }); @override Widget build(BuildContext context) { - final items = dataSource.map((value) => DropdownMenuItem( + return Checkbox( value: value, - child: Text(value.toString()), - )).toList(); - - return DropdownButtonFormField( - decoration: InputDecoration( - labelText: label, - hintText: '選択してください', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - value: initialSelectedValue != null ? initialSelectedValue : null, - items: items, - onChanged: onChanged, + onChanged: onChangedCallback, ); } } -/// テキストエリアフィールド(長文章用) -class MasterTextArea extends StatelessWidget { +/// マスタ編集用の Switch +class MasterSwitchField extends StatelessWidget { final String label; - final TextEditingController controller; - final String? hint; - final FormFieldValidator? validator; - // Nullable の形で定義 - final void Function(String)? onChanged; - final bool readOnly; + final bool value; + final ValueChanged? onChangedCallback; - const MasterTextArea({ + const MasterSwitchField({ super.key, required this.label, - required this.controller, - this.hint, - this.validator, - this.onChanged, - this.readOnly = false, + required this.value, + this.onChangedCallback, }); @override Widget build(BuildContext context) { - return TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: label, - hintText: hint, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - maxLines: 4, - readOnly: readOnly, - validator: (value) => onChanged?.call(value) ?? validator?.call(value), - onChanged: onChanged, - ); - } -} - -/// チェックボックスフィールド(フラグ用) -class MasterCheckBox extends StatelessWidget { - final String label; - final bool initialValue; - final FormFieldValidator? validator; - // SwitchListTile の onChanged は void Function(bool)? を要求 - // Validator とコールバックを分離する形に - final VoidCallback? onCheckedCallback; - - const MasterCheckBox({ - super.key, - required this.label, - required this.initialValue, - this.validator, - this.onCheckedCallback, - }); - - @override - Widget build(BuildContext context) { - return SwitchListTile( - title: Text(label), - subtitle: initialValue ? const Text('有効') : const Text('無効'), - value: initialValue, - onChanged: (value) { - if (validator?.call(value) != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(validator!.call(value) ?? ''), backgroundColor: Colors.red), - ); - } else if (onCheckedCallback?.call() ?? false) { - onCheckedCallback?.call(); - } - }, + return Switch( + value: value, + onChanged: onChangedCallback, ); } } \ No newline at end of file