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:
parent
13f7e3fcc6
commit
9cec464868
14 changed files with 338 additions and 269 deletions
103
@workspace/lib/pdf_templates/estimate_template.dart
Normal file
103
@workspace/lib/pdf_templates/estimate_template.dart
Normal file
|
|
@ -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<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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
@workspace/lib/widgets/master_edit_fields.dart
Normal file
201
@workspace/lib/widgets/master_edit_fields.dart
Normal 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
168
README.md
|
|
@ -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/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」として管理・参照してください。
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()) :
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,),
|
||||||
|
|
|
||||||
|
|
@ -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,),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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>(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue