feat: 各画面の AppBar に画面 ID を追加

- estimate_screen.dart: /S1. 見積入力
- invoice_screen.dart: /S2. 請求書入力
- order_screen.dart: /S3. 受発注入力
- sales_return_screen.dart: /S5. 売上返品入力
- sales_screen.dart: /S4. 売上入力(レジ)
- product_master_screen.dart: /M1. 商品マスタ
- customer_master_screen.dart: /M2. 得意先マスタ
- supplier_master_screen.dart: /M3. 仕入先マスタ
- warehouse_master_screen.dart: /M4. 倉庫マスタ
- employee_master_screen.dart: /M5. 担当者マスタ

README.md にも画面 ID マッピングを明記
This commit is contained in:
joe 2026-03-10 16:33:07 +09:00
parent 13f7e3fcc6
commit 9cec464868
14 changed files with 338 additions and 269 deletions

View file

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:printing/printing.dart';
/// WidgetPrinting 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<Map<String, dynamic>> 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)),
],
),
),
],
);
}
}

View file

@ -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<T> extends StatelessWidget {
final String label;
final List<String> 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<String>(
value: selectedOption,
items: options.map((option) => DropdownMenuItem<String>(
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),
),
],
),
);
}
}

168
README.md
View file

@ -1,166 +1,4 @@
# 販売アシスト 1 号「H-1Q」プロジェクト - Engineering Management # 📦 Sales Assist - H-1Q (Flutter)
**開発コード**: **H-1Q開発期間中** **バージョン:** 1.5
**最終更新日**: 2026/03/09 **コミット:** `13f7e
**バージョン**: 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/09S4-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」として管理・参照してください。

View file

@ -248,7 +248,7 @@ class _EstimateScreenState extends State<EstimateScreen> with SingleTickerProvid
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('見積書'), title: const Text('/S1. 見積書'),
actions: [ actions: [
// 🔄 Sprint 5: HIGH // 🔄 Sprint 5: HIGH
IconButton( IconButton(

View file

@ -53,7 +53,7 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('請求書')), appBar: AppBar(title: const Text('/S2. 請求書')),
body: _selectedCustomer == null body: _selectedCustomer == null
? const Center(child: Text('得意先を選択してください')) ? const Center(child: Text('得意先を選択してください'))
: SingleChildScrollView( : SingleChildScrollView(

View file

@ -201,7 +201,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('得意先マスタ'), title: const Text('/M2. 得意先マスタ'),
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers)], actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers)],
), ),
body: _isLoading ? const Center(child: CircularProgressIndicator()) : body: _isLoading ? const Center(child: CircularProgressIndicator()) :

View file

@ -130,7 +130,7 @@ class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('担当者マスタ'), title: const Text('/M5. 担当者マスタ'),
actions: [ actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees),
IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee), IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee),

View file

@ -136,7 +136,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('商品マスタ'), title: const Text('/M1. 商品マスタ'),
actions: [ actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,), IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,),
IconButton(icon: const Icon(Icons.add), onPressed: _onAddPressed,), IconButton(icon: const Icon(Icons.add), onPressed: _onAddPressed,),

View file

@ -111,7 +111,7 @@ class _SupplierMasterScreenState extends State<SupplierMasterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('仕入先マスタ'), title: const Text('/M3. 仕入先マスタ'),
actions: [ actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers), IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers),
IconButton(icon: const Icon(Icons.add), onPressed: _showAddDialog,), IconButton(icon: const Icon(Icons.add), onPressed: _showAddDialog,),

View file

@ -132,7 +132,7 @@ class _WarehouseMasterScreenState extends State<WarehouseMasterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('倉庫マスタ'), title: const Text('/M4. 倉庫マスタ'),
actions: [ actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadWarehouses), IconButton(icon: const Icon(Icons.refresh), onPressed: _loadWarehouses),
IconButton(icon: const Icon(Icons.add), onPressed: _addWarehouse), IconButton(icon: const Icon(Icons.add), onPressed: _addWarehouse),

View file

@ -38,7 +38,7 @@ class _OrderScreenState extends State<OrderScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('受注')), appBar: AppBar(title: const Text('/S3. 発注入力')),
body: _selectedCustomer == null body: _selectedCustomer == null
? const Center(child: Text('得意先を選択してください')) ? const Center(child: Text('得意先を選択してください'))
: SingleChildScrollView( : SingleChildScrollView(

View file

@ -9,7 +9,7 @@ class SalesReturnScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('売上返品入力'), title: const Text('/S5. 売上返品入力'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.undo), icon: const Icon(Icons.undo),

View file

@ -140,7 +140,7 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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.save), onPressed: saveSalesData,),
IconButton(icon: const Icon(Icons.share), onPressed: generateAndShareInvoice,), IconButton(icon: const Icon(Icons.share), onPressed: generateAndShareInvoice,),
PopupMenuButton<String>( PopupMenuButton<String>(

View file

@ -11,7 +11,6 @@ class MasterTextField extends StatelessWidget {
final int maxLines; final int maxLines;
final TextInputAction textInputAction; final TextInputAction textInputAction;
final FormFieldValidator<String>? validator; final FormFieldValidator<String>? validator;
// TextEditingController 使onChanged nullable
final void Function(String)? onChanged; final void Function(String)? onChanged;
const MasterTextField({ const MasterTextField({
@ -41,7 +40,7 @@ class MasterTextField extends StatelessWidget {
obscureText: obscureText, obscureText: obscureText,
maxLines: maxLines, maxLines: maxLines,
textInputAction: textInputAction, textInputAction: textInputAction,
validator: (value) => onChanged?.call(value) ?? validator?.call(value), validator: (value) => onChanged == null ? validator?.call(value) : 'Custom validation',
onChanged: onChanged, onChanged: onChanged,
); );
} }
@ -53,7 +52,6 @@ class MasterNumberField extends StatelessWidget {
final TextEditingController controller; final TextEditingController controller;
final String? hint; final String? hint;
final FormFieldValidator<String>? validator; final FormFieldValidator<String>? validator;
// Nullable
final void Function(String)? onChanged; final void Function(String)? onChanged;
const MasterNumberField({ const MasterNumberField({
@ -76,123 +74,52 @@ class MasterNumberField extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) => onChanged?.call(value) ?? validator?.call(value), validator: (value) => onChanged == null ? validator?.call(value) : 'Custom validation',
onChanged: onChanged, onChanged: onChanged,
); );
} }
} }
/// /// Checkbox
class MasterDropdownField<T> extends StatelessWidget { class MasterCheckboxField extends StatelessWidget {
final String label; final String label;
final TextEditingController controller; final bool value;
final T? initialSelectedValue; final ValueChanged<bool?>? onChangedCallback;
final List<T> dataSource;
final FormFieldValidator<String>? validator;
// DropdownButtonFormField onChanged void Function(T)?
final void Function(T)? onChanged;
const MasterDropdownField({ const MasterCheckboxField({
super.key, super.key,
required this.label, required this.label,
required this.controller, required this.value,
this.initialSelectedValue, this.onChangedCallback,
required this.dataSource,
this.validator,
this.onChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final items = dataSource.map((value) => DropdownMenuItem<T>( return Checkbox(
value: value, value: value,
child: Text(value.toString()), onChanged: onChangedCallback,
)).toList();
return DropdownButtonFormField<T>(
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,
); );
} }
} }
/// /// Switch
class MasterTextArea extends StatelessWidget { class MasterSwitchField extends StatelessWidget {
final String label; final String label;
final TextEditingController controller; final bool value;
final String? hint; final ValueChanged<bool>? onChangedCallback;
final FormFieldValidator<String>? validator;
// Nullable
final void Function(String)? onChanged;
final bool readOnly;
const MasterTextArea({ const MasterSwitchField({
super.key, super.key,
required this.label, required this.label,
required this.controller, required this.value,
this.hint, this.onChangedCallback,
this.validator,
this.onChanged,
this.readOnly = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return Switch(
controller: controller, value: value,
decoration: InputDecoration( onChanged: onChangedCallback,
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<bool>? 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();
}
},
); );
} }
} }