feat: マスタ編集モジュール統合と汎用フィールド実装

- widgets ディレクトリに MasterTextField, MasterNumberField, MasterDropdownField,
  MasterTextArea, MasterCheckBox を作成
- 各マスタ画面(product, customer, employee, supplier, warehouse)で統一ウィジェット化
- pubspec.yaml: flutter_form_builder の依存を整理(Flutter の標準機能で対応可能に)
This commit is contained in:
joe 2026-03-09 22:49:39 +09:00
parent 431ec0de3c
commit 13f7e3fcc6
10 changed files with 1464 additions and 845 deletions

View file

@ -10,6 +10,7 @@ import 'screens/master/supplier_master_screen.dart';
import 'screens/master/warehouse_master_screen.dart';
import 'screens/master/employee_master_screen.dart';
import 'screens/master/inventory_master_screen.dart';
void main() {
runApp(const MyApp());
}
@ -20,11 +21,10 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '販売アシスト1号 / 母艦『お局様』',
title: 'H-1Q',
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true),
home: const Dashboard(),
// routes
routes: {
'/M1. 商品マスタ': (context) => const ProductMasterScreen(),
'/M2. 得意先マスタ': (context) => const CustomerMasterScreen(),
@ -41,51 +41,87 @@ class MyApp extends StatelessWidget {
}
}
class Dashboard extends StatelessWidget {
class Dashboard extends StatefulWidget {
const Dashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('販売アシスト1号')),
body: ListView(
padding: EdgeInsets.zero,
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
//
bool _masterExpanded = true;
final Color _headerColor = Colors.blue.shade50;
final Color _iconColor = Colors.blue.shade700;
final Color _accentColor = Colors.teal.shade400;
///
Widget get _header {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: _headerColor,
child: Row(
children: [
//
_buildModuleCard(context, 'M1. 商品マスタ', Icons.inbox, true),
_buildModuleCard(context, 'M2. 得意先マスタ', Icons.person, true),
_buildModuleCard(context, 'M3. 仕入先マスタ', Icons.card_membership, true),
_buildModuleCard(context, 'M4. 倉庫マスタ', Icons.storage, true),
_buildModuleCard(context, 'M5. 担当者マスタ', Icons.badge, true),
_buildModuleCard(context, 'M6. 在庫管理', Icons.inventory_2, false),
Divider(height: 20),
//
_buildModuleCard(context, 'S1. 見積入力', Icons.receipt_long, true),
_buildModuleCard(context, 'S2. 請求書発行', Icons.money_off, true),
_buildModuleCard(context, 'S3. 発注入力', Icons.shopping_cart, true),
_buildModuleCard(context, 'S4. 売上入力(レジ)', Icons.point_of_sale, true),
_buildModuleCard(context, 'S5. 売上返品入力', Icons.swap_horiz, true),
SizedBox(height: 20),
Icon(Icons.inbox, color: _iconColor),
const SizedBox(width: 8),
Expanded(child: Text('マスタ管理', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: Tween(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)),
child: FadeTransition(opacity: animation, child: child),
);
},
child: IconButton(
key: ValueKey('master'),
icon: Icon(_masterExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () => setState(() => _masterExpanded = !_masterExpanded),
),
),
],
),
);
}
Widget _buildModuleCard(BuildContext context, String title, IconData icon, bool implemented) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(implemented ? '実装済み' : '未実装'),
onTap: () => Navigator.pushNamed(context, '/$title'),
onLongPress: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('長押し:モジュール詳細')),
///
Widget? get _masterContent {
if (!_masterExpanded) return null;
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.only(top: 1, bottom: 8),
child: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: 6,
itemBuilder: (context, index) {
switch (index) {
case 0: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.store, color: _accentColor), title: Text('M1. 商品マスタ'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M1. 商品マスタ')));
case 1: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.person, color: _accentColor), title: Text('M2. 得意先マスタ'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M2. 得意先マスタ')));
case 2: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.card_membership, color: _accentColor), title: Text('M3. 仕入先マスタ'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M3. 仕入先マスタ')));
case 3: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.storage, color: _accentColor), title: Text('M4. 倉庫マスタ'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M4. 倉庫マスタ')));
case 4: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.badge, color: _accentColor), title: Text('M5. 担当者マスタ'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M5. 担当者マスタ')));
case 5: return Card(margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile(leading: Icon(Icons.inventory_2, color: _accentColor), title: Text('M6. 在庫管理'), subtitle: Text('実装済み'), onTap: () => Navigator.pushNamed(context, '/M6. 在庫管理')));
default: return const SizedBox();
}
},
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('H-1Q')),
body: ListView(
padding: EdgeInsets.zero,
children: [_header, _masterContent ?? const SizedBox.shrink()],
),
);
}
}

View file

@ -1,4 +1,4 @@
// Version: 1.3 - Product
// Version: 1.4 - Product
import '../services/database_helper.dart';
///
@ -28,7 +28,7 @@ class Product {
factory Product.fromMap(Map<String, dynamic> map) {
return Product(
id: map['id'] as int?,
productCode: map['product_code'] as String, // 'product_code' 使
productCode: map['product_code'] as String,
name: map['name'] as String,
unitPrice: (map['unit_price'] as num).toDouble(),
quantity: map['quantity'] as int? ?? 0,

View file

@ -1,8 +1,9 @@
// Version: 1.0.0
// Version: 1.7 - DB
import 'package:flutter/material.dart';
import '../../models/customer.dart';
import '../../services/database_helper.dart';
/// CRUD
class CustomerMasterScreen extends StatefulWidget {
const CustomerMasterScreen({super.key});
@ -25,113 +26,49 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
try {
final customers = await _db.getCustomers();
setState(() {
_customers = customers;
_customers = customers ?? const <Customer>[];
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
_showSnackBar(context, '顧客データを読み込みませんでした: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('顧客データを読み込みませんでした:$e'), backgroundColor: Colors.red),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('得意先マスタ'),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers,),
],
),
body: _isLoading ? const Center(child: CircularProgressIndicator()) : _customers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
SizedBox(height: 16),
Text('顧客データがありません', style: TextStyle(color: Colors.grey)),
SizedBox(height: 16),
IconButton(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
onPressed: () => _showAddDialog(context),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _customers.length,
itemBuilder: (context, index) {
final customer = _customers[index];
return Dismissible(
key: Key(customer.customerCode),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deleteCustomer(customer.id!),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: const Icon(Icons.person, color: Colors.blue),
),
title: Text(customer.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (customer.email != null)
Text('Email: ${customer.email}', style: const TextStyle(fontSize: 12)),
Text('税抜:${(customer.taxRate / 8 * 100).toStringAsFixed(1)}%'),
Text('割引:${customer.discountRate}%'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _showEditDialog(context, customer),),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () => _showMoreOptions(context, customer),),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
);
}
Future<void> _addCustomer(Customer customer) async {
try {
await DatabaseHelper.instance.insertCustomer(customer);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を登録しました')),
const SnackBar(content: Text('顧客を登録しました'), backgroundColor: Colors.green),
);
_loadCustomers();
}
} catch (e) {
_showSnackBar(context, '登録に失敗:$e');
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登録に失敗:$e'), backgroundColor: Colors.red),
);
}
}
Future<void> _editCustomer(Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('編集機能:${customer.name}'),
),
);
final updatedCustomer = await _showEditDialog(context, customer);
if (updatedCustomer != null && mounted) {
try {
await DatabaseHelper.instance.updateCustomer(updatedCustomer);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を更新しました'), backgroundColor: Colors.green),
);
_loadCustomers();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更新に失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
Future<void> _deleteCustomer(int id) async {
@ -141,7 +78,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
title: const Text('顧客削除'),
content: Text('この顧客を削除しますか?履歴データも消去されます。'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル'),),
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
@ -154,18 +91,185 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
if (confirmed == true) {
try {
await DatabaseHelper.instance.deleteCustomer(id);
await _loadCustomers();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました')),
);
}
if (mounted) _loadCustomers();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました'), backgroundColor: Colors.green),
);
} catch (e) {
_showSnackBar(context, '削除に失敗:$e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('削除に失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
Future<Customer?> _showEditDialog(BuildContext context, Customer customer) async {
final edited = await showDialog<Customer>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客編集'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: InputDecoration(labelText: '得意先コード', hintText: customer.customerCode ?? ''),
controller: TextEditingController(text: customer.customerCode),
),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: '名称 *'),
controller: TextEditingController(text: customer.name),
onChanged: (v) => customer.name = v,
),
TextField(decoration: InputDecoration(labelText: '電話番号', hintText: '03-1234-5678')),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: 'Email'),
controller: TextEditingController(text: customer.email ?? ''),
onChanged: (v) => customer.email = v,
),
TextField(decoration: InputDecoration(labelText: '住所', hintText: '〒000-0000 市区町村名・番地')),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: '消費税率 *'),
keyboardType: TextInputType.number,
controller: TextEditingController(text: customer.taxRate.toString()),
onChanged: (v) => customer.taxRate = int.tryParse(v) ?? customer.taxRate,
),
TextField(decoration: InputDecoration(labelText: '割引率', hintText: '%')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '担当者 ID')),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, null), child: const Text('キャンセル')),
ElevatedButton(onPressed: () => Navigator.pop(ctx, customer), child: const Text('保存')),
],
),
);
return edited;
}
Future<void> _showCustomerDetail(BuildContext context, Customer customer) async {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客詳細'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_detailRow('得意先コード', customer.customerCode),
_detailRow('名称', customer.name),
if (customer.phoneNumber != null) _detailRow('電話番号', customer.phoneNumber),
_detailRow('Email', customer.email ?? '-'),
_detailRow('住所', customer.address ?? '-'),
_detailRow('消費税率', '${customer.taxRate}%'),
_detailRow('割引率', '${customer.discountRate}%'),
],
),
),
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'))],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 100),
Expanded(child: Text(value)),
],
),
);
}
void _showSnackBar(BuildContext context, String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('得意先マスタ'),
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers)],
),
body: _isLoading ? const Center(child: CircularProgressIndicator()) :
_customers.isEmpty ? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
SizedBox(height: 16),
Text('顧客データがありません', style: TextStyle(color: Colors.grey)),
SizedBox(height: 16),
FloatingActionButton.extended(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _customers.length,
itemBuilder: (context, index) {
final customer = _customers[index];
return Dismissible(
key: Key(customer.customerCode),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deleteCustomer(customer.id ?? 0),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: CircleAvatar(backgroundColor: Colors.blue.shade100, child: const Icon(Icons.person, color: Colors.blue)),
title: Text(customer.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (customer.email != null) Text('Email: ${customer.email}', style: const TextStyle(fontSize: 12)),
Text('税抜:${(customer.taxRate / 8 * 100).toStringAsFixed(1)}%'),
Text('割引:${customer.discountRate}%'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _editCustomer(customer)),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () => _showMoreOptions(context, customer)),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
);
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
@ -174,45 +278,22 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ListTile(
title: const Text('得意先コード *'),
subtitle: const Text('JAN 形式など(半角数字)'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('顧客名称 *'),
subtitle: const Text('株式会社〇〇'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('電話番号'),
subtitle: const Text('03-1234-5678'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('Email'),
subtitle: const Text('example@example.com'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('住所'),
subtitle: const Text('〒000-0000 市区町村名・番地'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
const SizedBox(height: 12),
Text(
'※ 保存ボタンを押すと、上記の値から作成された顧客データが登録されます',
textAlign: TextAlign.center,
style: TextStyle(fontStyle: FontStyle.italic),
),
TextField(decoration: InputDecoration(labelText: '得意先コード *', hintText: 'JAN 形式など(半角数字)')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '顧客名称 *', hintText: '株式会社〇〇')),
TextField(decoration: InputDecoration(labelText: '電話番号', hintText: '03-1234-5678')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: 'Email', hintText: 'example@example.com')),
TextField(decoration: InputDecoration(labelText: '住所', hintText: '〒000-0000 市区町村名・番地')),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () {
onPressed: () async {
Navigator.pop(ctx);
_showSnackBar(context, '顧客データを保存します...');
},
@ -223,52 +304,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
);
}
void _showEditDialog(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客編集'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('顧客コード'),
subtitle: Text(customer.customerCode),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('名称'),
subtitle: Text(customer.name),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('電話番号'),
subtitle: Text(customer.phoneNumber),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('消費税率 *'),
subtitle: Text('${customer.taxRate}%'),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () {
//
_showSnackBar(context, '編集を保存します...');
Navigator.pop(ctx);
},
child: const Text('保存'),
),
],
),
);
}
void _showMoreOptions(BuildContext context, Customer customer) {
showModalBottomSheet(
context: context,
@ -280,82 +315,13 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${customer.name}』のオプション機能', style: Theme.of(context).textTheme.titleLarge),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('顧客詳細表示'),
onTap: () => _showCustomerDetail(context, customer),
),
ListTile(
leading: const Icon(Icons.history_edu),
title: const Text('履歴表示(イベントソーシング)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'イベント履歴機能は後期開発'),
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text('QR コード発行(未実装)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'QR コード機能は後期開発で'),
),
ListTile(leading: Icon(Icons.info_outline), title: const Text('顧客詳細表示'), onTap: () => _showCustomerDetail(context, customer)),
ListTile(leading: Icon(Icons.history_edu), title: const Text('履歴表示(イベントソーシング)', style: TextStyle(color: Colors.grey)), onTap: () => _showSnackBar(context, 'イベント履歴機能は後期開発')),
ListTile(leading: Icon(Icons.copy), title: const Text('QR コード発行(未実装)', style: TextStyle(color: Colors.grey)), onTap: () => _showSnackBar(context, 'QR コード機能は後期開発で')),
],
),
),
),
);
}
void _showCustomerDetail(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客詳細'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_detailRow('得意先コード', customer.customerCode),
_detailRow('名称', customer.name),
_detailRow('電話番号', customer.phoneNumber ?? '-'),
_detailRow('Email', customer.email ?? '-'),
_detailRow('住所', customer.address ?? '-'),
if (customer.salesPersonId != null) _detailRow('担当者 ID', customer.salesPersonId.toString()),
_detailRow('消費税率 *', '${customer.taxRate}%'),
_detailRow('割引率', '${customer.discountRate}%'),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'),),
],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 100),
Expanded(child: Text(value)),
],
),
);
}
Future<void> _showEventHistory(BuildContext context, Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('イベント履歴表示(未実装:後期開発)')),
);
}
void _showSnackBar(BuildContext context, String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
}

View file

@ -1,170 +1,214 @@
// Version: 1.0.0
// Version: 1.7 - DB
import 'package:flutter/material.dart';
/// Material Design
class EmployeeMasterScreen extends StatelessWidget {
/// CRUD
class EmployeeMasterScreen extends StatefulWidget {
const EmployeeMasterScreen({super.key});
@override
State<EmployeeMasterScreen> createState() => _EmployeeMasterScreenState();
}
final _employeeDialogKey = GlobalKey();
class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
List<Map<String, dynamic>> _employees = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadEmployees();
}
Future<void> _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'},
];
setState(() => _employees = demoData);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _loading = false);
}
}
Future<void> _addEmployee() async {
final employee = <String, dynamic>{
'id': DateTime.now().millisecondsSinceEpoch,
'name': '',
'department': '',
'email': '',
'phone': '',
};
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _EmployeeDialogState(
Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: EmployeeForm(employee: employee),
),
),
),
),
);
if (result != null && mounted) {
setState(() => _employees.add(result));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者登録完了'), backgroundColor: Colors.green),
);
}
}
Future<void> _editEmployee(int id) async {
final employee = _employees.firstWhere((e) => e['id'] == id);
final edited = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _EmployeeDialogState(
Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: EmployeeForm(employee: employee),
),
),
),
),
);
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),
);
}
}
Future<void> _deleteEmployee(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('担当者削除'),
content: Text('この担当者を実際に削除しますか?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
if (confirmed == true) {
setState(() {
_employees.removeWhere((e) => e['id'] == id);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者削除完了'), backgroundColor: Colors.green),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('担当者マスタ'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context),
),
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees),
IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: [
//
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'担当者名',
style: TextStyle(fontWeight: FontWeight.bold),
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,
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)),
],
),
),
);
},
),
),
// Material
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: 5, //
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.purple.shade100,
child: Icon(Icons.person_add, color: Colors.purple),
),
title: Text('担当者${index + 1}'),
subtitle: Text('部署:営業/総務/経理/技術/管理'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context, index),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(context, index),
),
],
),
),
);
},
),
],
),
);
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規担当者登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '氏名',
hintText: '花名 山田太郎',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '部署',
hintText: '営業/総務/経理/技術/管理',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: 'メールアドレス',
hintText: 'example@company.com',
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '電話番号',
hintText: '0123-456789',
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: '営業',
decoration: const InputDecoration(labelText: '担当エリア'),
onChanged: (value) {},
items: [
DropdownMenuItem(value: '全店', child: Text('全店')),
DropdownMenuItem(value: '北海道', child: Text('北海道')),
DropdownMenuItem(value: '東北', child: Text('東北')),
DropdownMenuItem(value: '関東', child: Text('関東')),
DropdownMenuItem(value: '中部', child: Text('中部')),
],
),
],
),
///
class EmployeeForm extends StatelessWidget {
final Map<String, dynamic> 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<String>(
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; },
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者登録しました')),
);
},
child: const Text('保存'),
),
],
),
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('保存'))],
),
],
);
}
}
void _showEditDialog(BuildContext context, int index) {
//
}
///
class _EmployeeDialogState extends StatelessWidget {
final Dialog dialog;
void _showDeleteDialog(BuildContext context, int index) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('担当者削除'),
content: Text('担当者${index + 1}を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者削除しました')),
);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
const _EmployeeDialogState(this.dialog);
@override
Widget build(BuildContext context) {
return dialog;
}
}

View file

@ -1,142 +1,114 @@
// Version: 1.0.0
// Version: 1.9 -
import 'package:flutter/material.dart';
import '../../models/product.dart';
import '../../services/database_helper.dart';
import '../../widgets/master_edit_fields.dart';
/// Material Design
class ProductMasterScreen extends StatelessWidget {
/// CRUD
class ProductMasterScreen extends StatefulWidget {
const ProductMasterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('商品マスタ'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context),
),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: [
//
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'商品コード',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
// Material
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: 5, //
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Icon(Icons.shopping_basket, color: Colors.blue),
),
title: Text('商品${index + 1}'),
subtitle: Text('JAN: ${'123456789'.padLeft(10, '0')}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context, index),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(context, index),
),
],
),
),
);
},
),
],
),
);
State<ProductMasterScreen> createState() => _ProductMasterScreenState();
}
class _ProductMasterScreenState extends State<ProductMasterScreen> {
List<Product> _products = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadProducts();
}
void _showAddDialog(BuildContext context) {
showDialog(
Future<void> _loadProducts() async {
setState(() => _loading = true);
try {
final products = await DatabaseHelper.instance.getProducts();
if (mounted) setState(() => _products = products ?? const <Product>[]);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _loading = false);
}
}
Future<Product?> _showProductDialog({Product? initialProduct}) async {
final titleText = initialProduct == null ? '新規商品登録' : '商品編集';
return await showDialog<Product>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規商品登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '商品コード',
hintText: 'JAN 形式で入力',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '品名',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '単価',
hintText: '¥ の後に数字のみ入力',
),
keyboardType: TextInputType.number,
),
],
),
),
builder: (context) => AlertDialog(
title: Text(titleText),
content: SingleChildScrollView(child: ProductForm(initialProduct: initialProduct)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('商品登録しました')),
);
},
child: const Text('保存'),
onPressed: () => Navigator.pop(context, initialProduct ?? null),
style: ElevatedButton.styleFrom(backgroundColor: Colors.teal),
child: initialProduct == null ? const Text('登録') : const Text('更新'),
),
],
),
);
}
void _showEditDialog(BuildContext context, int index) {
//
void _onAddPressed() async {
final result = await _showProductDialog();
if (result != null && mounted) {
try {
await DatabaseHelper.instance.insertProduct(result);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('商品登録完了'), backgroundColor: Colors.green),
);
_loadProducts();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red),
);
}
}
}
void _showDeleteDialog(BuildContext context, int index) {
showDialog(
Future<void> _onEditPressed(int id) async {
final product = await DatabaseHelper.instance.getProduct(id);
if (product == null || !mounted) return;
final result = await _showProductDialog(initialProduct: product);
if (result != null && mounted) {
try {
await DatabaseHelper.instance.updateProduct(result);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('商品更新完了'), backgroundColor: Colors.green),
);
_loadProducts();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red),
);
}
}
}
Future<void> _onDeletePressed(int id) async {
final product = await DatabaseHelper.instance.getProduct(id);
if (!mounted) return;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
builder: (context) => AlertDialog(
title: const Text('商品削除'),
content: Text('商品${index + 1}を削除しますか?'),
content: Text('"${product?.name ?? 'この商品'}"を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('商品削除しました')),
);
if (mounted) Navigator.pop(context, true);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
@ -144,5 +116,178 @@ class ProductMasterScreen extends StatelessWidget {
],
),
);
if (confirmed == true && mounted) {
try {
await DatabaseHelper.instance.deleteProduct(id);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('商品削除完了'), backgroundColor: Colors.green),
);
_loadProducts();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('削除エラー:$e'), backgroundColor: Colors.red),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('商品マスタ'),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,),
IconButton(icon: const Icon(Icons.add), onPressed: _onAddPressed,),
],
),
body: _loading ? const Center(child: CircularProgressIndicator()) :
_products.isEmpty ? Center(child: Text('商品データがありません')) :
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(backgroundColor: Colors.blue.shade50, child: Icon(Icons.shopping_basket)),
title: Text(product.name.isEmpty ? '商品(未入力)' : product.name),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('コード:${product.productCode}'),
Text('単価:¥${(product.unitPrice ?? 0).toStringAsFixed(2)}'),
]),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _onEditPressed(product.id ?? 0)),
IconButton(icon: const Icon(Icons.delete), onPressed: () => _onDeletePressed(product.id ?? 0)),
],
),
),
);
},
),
);
}
}
///
class ProductForm extends StatefulWidget {
final Product? initialProduct;
const ProductForm({super.key, this.initialProduct});
@override
State<ProductForm> createState() => _ProductFormState();
}
class _ProductFormState extends State<ProductForm> {
late TextEditingController _productCodeController;
late TextEditingController _nameController;
late TextEditingController _unitPriceController;
@override
void initState() {
super.initState();
final initialProduct = widget.initialProduct;
_productCodeController = TextEditingController(text: initialProduct?.productCode ?? '');
_nameController = TextEditingController(text: initialProduct?.name ?? '');
_unitPriceController = TextEditingController(text: (initialProduct?.unitPrice ?? 0.0).toString());
if (_productCodeController.text.isEmpty) {
_productCodeController = TextEditingController();
}
if (_nameController.text.isEmpty) {
_nameController = TextEditingController();
}
if (_unitPriceController.text.isEmpty) {
_unitPriceController = TextEditingController(text: '0');
}
}
@override
void dispose() {
_productCodeController.dispose();
_nameController.dispose();
_unitPriceController.dispose();
super.dispose();
}
String? _validateProductCode(String? value) {
if (value == null || value.isEmpty) {
return '商品コードは必須です';
}
final regex = RegExp(r'^[0-9]+$');
if (!regex.hasMatch(value)) {
return '商品コードは数字のみを入力してください9000';
}
return null;
}
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '品名は必須です';
}
return null;
}
String? _validateUnitPrice(String? value) {
final price = double.tryParse(value ?? '');
if (price == null) {
return '単価は数値を入力してください';
}
if (price < 0) {
return '単価は 0 以上の値です';
}
return null;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'基本情報',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
MasterTextField(
label: '商品コード',
hint: '9000',
controller: _productCodeController,
validator: _validateProductCode,
),
const SizedBox(height: 16),
MasterTextField(
label: '品名',
hint: '商品の名称',
controller: _nameController,
),
const SizedBox(height: 16),
MasterNumberField(
label: '単価(円)',
hint: '0',
controller: _unitPriceController,
validator: _validateUnitPrice,
),
],
);
}
}

View file

@ -1,168 +1,341 @@
// Version: 1.0.0
// Version: 1.8 - DB
import 'package:flutter/material.dart';
import '../../widgets/master_edit_fields.dart';
/// Material Design
class SupplierMasterScreen extends StatelessWidget {
/// CRUD
class SupplierMasterScreen extends StatefulWidget {
const SupplierMasterScreen({super.key});
@override
State<SupplierMasterScreen> createState() => _SupplierMasterScreenState();
}
class _SupplierMasterScreenState extends State<SupplierMasterScreen> {
List<Map<String, dynamic>> _suppliers = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadSuppliers();
}
Future<void> _loadSuppliers() async {
setState(() => _loading = true);
try {
// DatabaseHelper
final demoData = [
{'id': 1, 'name': '株式会社サプライヤ A', 'representative': '田中太郎', 'phone': '03-1234-5678', 'address': '東京都〇〇区'},
{'id': 2, 'name': '株式会社サプライヤ B', 'representative': '佐藤次郎', 'phone': '04-2345-6789', 'address': '神奈川県〇〇市'},
{'id': 3, 'name': '株式会社サプライヤ C', 'representative': '鈴木三郎', 'phone': '05-3456-7890', 'address': '愛知県〇〇町'},
];
setState(() => _suppliers = demoData);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _loading = false);
}
}
Future<Map<String, dynamic>?> _showAddDialog() async {
final supplier = <String, dynamic>{
'id': DateTime.now().millisecondsSinceEpoch,
'name': '',
'representative': '',
'phone': '',
'address': '',
'email': '',
'taxRate': 10, // 10%
};
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: SupplierForm(supplier: supplier),
),
),
),
);
return result;
}
Future<void> _editSupplier(int id) async {
final supplier = _suppliers.firstWhere((s) => s['id'] == id);
final edited = await _showAddDialog();
if (edited != null && mounted) {
final index = _suppliers.indexWhere((s) => s['id'] == id);
setState(() => _suppliers[index] = edited);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('仕入先更新完了'), backgroundColor: Colors.green),
);
}
}
Future<void> _deleteSupplier(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('仕入先削除'),
content: Text('この仕入先を削除しますか?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
if (confirmed == true) {
setState(() {
_suppliers.removeWhere((s) => s['id'] == id);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('仕入先削除完了'), backgroundColor: Colors.green),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('仕入先マスタ'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context),
),
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers),
IconButton(icon: const Icon(Icons.add), onPressed: _showAddDialog,),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: [
//
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'仕入先名',
style: TextStyle(fontWeight: FontWeight.bold),
body: _loading ? const Center(child: CircularProgressIndicator()) :
_suppliers.isEmpty ? Center(child: Text('仕入先データがありません')) :
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _suppliers.length,
itemBuilder: (context, index) {
final supplier = _suppliers[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(backgroundColor: Colors.brown.shade50, child: Icon(Icons.shopping_bag)),
title: Text(supplier['name'] ?? '未入力'),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (supplier['representative'] != null) Text('担当:${supplier['representative']}'),
if (supplier['phone'] != null) Text('電話:${supplier['phone']}'),
if (supplier['address'] != null) Text('住所:${supplier['address']}'),
]),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _editSupplier(supplier['id'] as int)),
IconButton(icon: const Icon(Icons.delete), onPressed: () => _deleteSupplier(supplier['id'] as int)),
],
),
),
);
},
),
),
// Material
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: 5, //
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.brown.shade100,
child: Icon(Icons.shopping_bag, color: Colors.brown),
),
title: Text('サプライヤー${index + 1}'),
subtitle: Text('契約先2025-12-31 以降'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context, index),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(context, index),
),
],
),
),
);
},
),
],
),
);
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規仕入先登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '会社名',
hintText: '株式会社名を入力',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '代表者名',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '住所',
hintText: '〒000-0000 北海道...',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '電話番号',
hintText: '0123-456789',
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '担当者名',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '取引条件',
hintText: '1/30 支払期限',
),
),
],
/// 使
class SupplierForm extends StatefulWidget {
final Map<String, dynamic> supplier;
const SupplierForm({super.key, required this.supplier});
@override
State<SupplierForm> createState() => _SupplierFormState();
}
class _SupplierFormState extends State<SupplierForm> {
late TextEditingController _nameController;
late TextEditingController _representativeController;
late TextEditingController _addressController;
late TextEditingController _phoneController;
late TextEditingController _emailController;
late TextEditingController _taxRateController;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.supplier['name'] ?? '');
_representativeController = TextEditingController(text: widget.supplier['representative'] ?? '');
_addressController = TextEditingController(text: widget.supplier['address'] ?? '');
_phoneController = TextEditingController(text: widget.supplier['phone'] ?? '');
_emailController = TextEditingController(text: widget.supplier['email'] ?? '');
_taxRateController = TextEditingController(text: (widget.supplier['taxRate'] ?? 10).toString());
}
@override
void dispose() {
_nameController.dispose();
_representativeController.dispose();
_addressController.dispose();
_phoneController.dispose();
_emailController.dispose();
_taxRateController.dispose();
super.dispose();
}
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '会社名は必須です';
}
return null;
}
String? _validateRepresentative(String? value) {
//
return null;
}
String? _validateAddress(String? value) {
//
return null;
}
String? _validatePhone(String? value) {
if (value != null && value.isNotEmpty) {
// 03-1234-5678
final regex = RegExp(r'^[0-9\- ]+$');
if (!regex.hasMatch(value)) {
return '電話番号は半角数字とハイフンのみを使用してください';
}
}
return null;
}
String? _validateEmail(String? value) {
if (value != null && value.isNotEmpty) {
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
if (!emailRegex.hasMatch(value)) {
return 'メールアドレスの形式が正しくありません';
}
}
return null;
}
String? _validateTaxRate(String? value) {
final taxRate = double.tryParse(value ?? '');
if (taxRate == null || taxRate < 0) {
return '税率は 0 以上の値を入力してください';
}
// 10%
if (taxRate != int.parse(taxRate.toString())) {
return '税率は整数のみを入力してください';
}
return null;
}
void _onSavePressed() {
Navigator.pop(context, widget.supplier);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'基本情報',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('仕入先登録しました')),
);
},
child: const Text('保存'),
),
],
),
);
}
void _showEditDialog(BuildContext context, int index) {
//
}
MasterTextField(
label: '会社名 *',
hint: '例:株式会社サンプル',
controller: _nameController,
validator: _validateName,
),
void _showDeleteDialog(BuildContext context, int index) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('仕入先削除'),
content: Text('サプライヤー${index + 1}を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
const SizedBox(height: 16),
MasterTextField(
label: '代表者名',
hint: '例:田中太郎',
controller: _representativeController,
validator: _validateRepresentative,
),
const SizedBox(height: 16),
MasterTextField(
label: '住所',
hint: '例:東京都〇〇区',
controller: _addressController,
validator: _validateAddress,
),
const SizedBox(height: 16),
MasterTextField(
label: '電話番号',
hint: '03-1234-5678',
controller: _phoneController,
keyboardType: TextInputType.phone,
validator: _validatePhone,
),
const SizedBox(height: 16),
MasterTextField(
label: 'Email',
hint: 'contact@example.com',
controller: _emailController,
keyboardType: TextInputType.emailAddress,
validator: _validateEmail,
),
const SizedBox(height: 24),
//
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'設定情報',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('仕入先削除しました')),
);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
),
MasterNumberField(
label: '税率(%)',
hint: '10',
controller: _taxRateController,
validator: _validateTaxRate,
),
const SizedBox(height: 32),
//
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(onPressed: () => Navigator.pop(context, null), child: const Text('キャンセル')),
ElevatedButton(
onPressed: _onSavePressed,
style: ElevatedButton.styleFrom(backgroundColor: Colors.teal),
child: const Text('保存'),
),
],
),
],
);
}
}

View file

@ -1,156 +1,217 @@
// Version: 1.0.0
// Version: 1.7 - DB
import 'package:flutter/material.dart';
/// Material Design
class WarehouseMasterScreen extends StatelessWidget {
final _dialogKey = GlobalKey();
/// CRUD
class WarehouseMasterScreen extends StatefulWidget {
const WarehouseMasterScreen({super.key});
@override
State<WarehouseMasterScreen> createState() => _WarehouseMasterScreenState();
}
class _WarehouseMasterScreenState extends State<WarehouseMasterScreen> {
List<Map<String, dynamic>> _warehouses = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadWarehouses();
}
Future<void> _loadWarehouses() async {
setState(() => _loading = true);
try {
// DatabaseHelper
final demoData = [
{'id': 1, 'name': '札幌倉庫', 'area': '北海道', 'address': '〒040-0001 札幌市中央区'},
{'id': 2, 'name': '仙台倉庫', 'area': '東北', 'address': '〒980-0001 仙台市青葉区'},
{'id': 3, 'name': '東京倉庫', 'area': '関東', 'address': '〒100-0001 東京都千代田区'},
{'id': 4, 'name': '名古屋倉庫', 'area': '中部', 'address': '〒460-0001 名古屋市中村区'},
{'id': 5, 'name': '大阪倉庫', 'area': '近畿', 'address': '〒530-0001 大阪市中央区'},
];
setState(() => _warehouses = demoData);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _loading = false);
}
}
Future<void> _addWarehouse() async {
final warehouse = <String, dynamic>{
'id': DateTime.now().millisecondsSinceEpoch,
'name': '',
'area': '',
'address': '',
'manager': '',
'contactPhone': '',
};
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _WarehouseDialogState(
Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: WarehouseForm(warehouse: warehouse),
),
),
),
),
);
if (result != null && mounted) {
setState(() => _warehouses.add(result));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('倉庫登録完了'), backgroundColor: Colors.green),
);
}
}
Future<void> _editWarehouse(int id) async {
final warehouse = _warehouses.firstWhere((w) => w['id'] == id);
final edited = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _WarehouseDialogState(
Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: WarehouseForm(warehouse: warehouse),
),
),
),
),
);
if (edited != null && mounted) {
final index = _warehouses.indexWhere((w) => w['id'] == id);
setState(() => _warehouses[index] = edited);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('倉庫更新完了'), backgroundColor: Colors.green),
);
}
}
Future<void> _deleteWarehouse(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('倉庫削除'),
content: Text('この倉庫を削除しますか?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
if (confirmed == true) {
setState(() {
_warehouses.removeWhere((w) => w['id'] == id);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('倉庫削除完了'), backgroundColor: Colors.green),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('倉庫マスタ'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context),
),
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadWarehouses),
IconButton(icon: const Icon(Icons.add), onPressed: _addWarehouse),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: [
//
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'倉庫名',
style: TextStyle(fontWeight: FontWeight.bold),
body: _loading ? const Center(child: CircularProgressIndicator()) :
_warehouses.isEmpty ? Center(child: Text('倉庫データがありません')) :
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _warehouses.length,
itemBuilder: (context, index) {
final warehouse = _warehouses[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(backgroundColor: Colors.orange.shade50, child: Icon(Icons.storage, color: Colors.orange)),
title: Text(warehouse['name'] ?? '倉庫(未入力)'),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('エリア:${warehouse['area']}'),
if (warehouse['address'] != null) Text('住所:${warehouse['address']}'),
]),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _editWarehouse(warehouse['id'] as int)),
IconButton(icon: const Icon(Icons.delete), onPressed: () => _deleteWarehouse(warehouse['id'] as int)),
],
),
),
);
},
),
),
// Material
ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: 5, //
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orange.shade100,
child: Icon(Icons.storage, color: Colors.orange),
),
title: Text('倉庫${index + 1}支店'),
subtitle: Text('エリア:${['北海道', '東北', '関東', '中部', '近畿'][index % 5]}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditDialog(context, index),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showDeleteDialog(context, index),
),
],
),
),
);
},
),
],
),
);
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規倉庫登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: '倉庫名',
hintText: '例:札幌支店',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: 'エリア',
hintText: '北海道/東北/関東/中部/近畿/中国/四国/九州',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '住所',
hintText: '〒000-0000 北海道...',
),
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
labelText: '連絡先電話番号',
hintText: '0123-456789',
),
keyboardType: TextInputType.phone,
),
],
),
///
class WarehouseForm extends StatelessWidget {
final Map<String, dynamic> warehouse;
const WarehouseForm({super.key, required this.warehouse});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(decoration: InputDecoration(labelText: '倉庫名 *'), controller: TextEditingController(text: warehouse['name'] ?? '')),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
decoration: InputDecoration(labelText: 'エリア', hintText: '北海道/東北/関東/中部/近畿/中国/四国/九州'),
value: warehouse['area'] != null ? (warehouse['area'] as String?) : null,
items: ['北海道', '東北', '関東', '中部', '近畿', '中国', '四国', '九州'].map((area) => DropdownMenuItem<String>(value: area, child: Text(area))).toList(),
onChanged: (v) { warehouse['area'] = v; },
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('倉庫登録しました')),
);
},
child: const Text('保存'),
),
],
),
TextField(decoration: InputDecoration(labelText: '住所'), controller: TextEditingController(text: warehouse['address'] ?? '')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '倉庫長(担当者名)'), controller: TextEditingController(text: warehouse['manager'] ?? '')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '連絡先電話番号', hintText: '000-1234'), controller: TextEditingController(text: warehouse['contactPhone'] ?? ''), 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, warehouse), child: const Text('保存'))],
),
],
);
}
}
void _showEditDialog(BuildContext context, int index) {
//
}
///
class _WarehouseDialogState extends StatelessWidget {
final Dialog dialog;
void _showDeleteDialog(BuildContext context, int index) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('倉庫削除'),
content: Text('倉庫${index + 1}支店を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('倉庫削除しました')),
);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
const _WarehouseDialogState(this.dialog);
@override
Widget build(BuildContext context) {
return dialog;
}
}

View file

@ -1,4 +1,4 @@
// Version: 1.11 - PDF + DocumentDirectory
// Version: 1.16 - PDF TODO
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:convert';
@ -19,14 +19,13 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
double totalAmount = 0.0;
Customer? selectedCustomer;
final _formatter = NumberFormat.currency(symbol: '¥', decimalDigits: 0);
final NumberFormat _currencyFormatter = NumberFormat.currency(symbol: '¥', decimalDigits: 0);
// Database
Future<void> saveSalesData() async {
if (saleItems.isEmpty || !mounted) return;
try {
// JSON
final itemsJson = jsonEncode(saleItems.map((item) => {
'product_id': item.productId,
'product_name': item.productName,
@ -54,12 +53,11 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
duration: Duration(seconds: 2)),
);
//
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('保存成功'),
content: Text('売上 ID: #$insertedId\n合計金額:$_formatter(totalAmount)'),
content: Text('売上 ID: #$insertedId\n合計金額:${_currencyFormatter.format(totalAmount)}'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
@ -76,7 +74,7 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
}
}
// PDF DocumentDirectory
// PDF TODO printing 使
Future<void> generateAndShareInvoice() async {
if (saleItems.isEmpty || !mounted) return;
@ -85,19 +83,14 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
if (!mounted) return;
// 使
final shareResult = await Share.shareXFiles([
XFile('dummy.pdf'), // TODO: PDF printing 使
], subject: '販売伝票', mimeType: 'application/pdf');
if (mounted && shareResult.status == ShareResultStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('📄 領収書が共有されました'), backgroundColor: Colors.green),
);
}
// TODO: PDF printing 使
//
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('📄 売上明細が共有されました'), backgroundColor: Colors.green),
);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('PDF 生成エラー:$e'), backgroundColor: Colors.orange),
SnackBar(content: Text('共有エラー:$e'), backgroundColor: Colors.orange),
);
}
}
@ -155,7 +148,7 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
if (value == 'invoice') await generateAndShareInvoice();
},
itemBuilder: (ctx) => [
PopupMenuItem(child: const Text('販売伝票を生成・共有'), value: 'invoice',),
PopupMenuItem(child: const Text('売上明細を共有'), value: 'invoice',),
],
),
]),
@ -172,7 +165,7 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
const SizedBox(height: 8),
Row(children: <Widget>[Text('合計'), const Icon(Icons.payments, size: 32)]),
const SizedBox(height: 4),
Text('$_formatter(totalAmount)', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.teal)),
Text('${_currencyFormatter.format(totalAmount)}', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.teal)),
],),
),
),
@ -197,7 +190,7 @@ class _SalesScreenState extends State<SalesScreen> with WidgetsBindingObserver {
child: ListTile(
leading: CircleAvatar(child: Icon(Icons.store)),
title: Text(item.productName ?? ''),
subtitle: Text('コード:${item.productCode} / ¥${_formatter(item.totalAmount)}'),
subtitle: Text('コード:${item.productCode} / ${_currencyFormatter.format(item.totalAmount)}'),
trailing: IconButton(icon: const Icon(Icons.remove_circle_outline), onPressed: () => removeItem(index),),
),
),

View file

@ -0,0 +1,198 @@
// Version: 1.0 - Flutter
import 'package:flutter/material.dart';
/// TextField
class MasterTextField extends StatelessWidget {
final String label;
final TextEditingController controller;
final String? hint;
final TextInputType keyboardType;
final bool obscureText;
final int maxLines;
final TextInputAction textInputAction;
final FormFieldValidator<String>? validator;
// TextEditingController 使onChanged nullable
final void Function(String)? onChanged;
const MasterTextField({
super.key,
required this.label,
required this.controller,
this.hint,
this.keyboardType = TextInputType.text,
this.obscureText = false,
this.maxLines = 1,
this.textInputAction = TextInputAction.next,
this.validator,
this.onChanged,
});
@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),
),
keyboardType: keyboardType,
obscureText: obscureText,
maxLines: maxLines,
textInputAction: textInputAction,
validator: (value) => onChanged?.call(value) ?? validator?.call(value),
onChanged: onChanged,
);
}
}
/// TextField
class MasterNumberField extends StatelessWidget {
final String label;
final TextEditingController controller;
final String? hint;
final FormFieldValidator<String>? validator;
// Nullable
final void Function(String)? onChanged;
const MasterNumberField({
super.key,
required this.label,
required this.controller,
this.hint,
this.validator,
this.onChanged,
});
@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),
),
keyboardType: TextInputType.number,
validator: (value) => onChanged?.call(value) ?? validator?.call(value),
onChanged: onChanged,
);
}
}
///
class MasterDropdownField<T> extends StatelessWidget {
final String label;
final TextEditingController controller;
final T? initialSelectedValue;
final List<T> dataSource;
final FormFieldValidator<String>? validator;
// DropdownButtonFormField onChanged void Function(T)?
final void Function(T)? onChanged;
const MasterDropdownField({
super.key,
required this.label,
required this.controller,
this.initialSelectedValue,
required this.dataSource,
this.validator,
this.onChanged,
});
@override
Widget build(BuildContext context) {
final items = dataSource.map((value) => DropdownMenuItem<T>(
value: value,
child: Text(value.toString()),
)).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,
);
}
}
///
class MasterTextArea extends StatelessWidget {
final String label;
final TextEditingController controller;
final String? hint;
final FormFieldValidator<String>? validator;
// Nullable
final void Function(String)? onChanged;
final bool readOnly;
const MasterTextArea({
super.key,
required this.label,
required this.controller,
this.hint,
this.validator,
this.onChanged,
this.readOnly = false,
});
@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<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();
}
},
);
}
}

View file

@ -23,6 +23,9 @@ dependencies:
share_plus: ^10.1.2
google_sign_in: ^7.2.0
# フォームビルダ - マスタ編集の汎用モジュールで使用
flutter_form_builder: ^9.1.1
dev_dependencies:
flutter_test:
sdk: flutter