diff --git a/ios/Flutter/Generated.xcconfig b/ios/Flutter/Generated.xcconfig index 0f5d1b2..60867b1 100644 --- a/ios/Flutter/Generated.xcconfig +++ b/ios/Flutter/Generated.xcconfig @@ -4,7 +4,7 @@ FLUTTER_APPLICATION_PATH=/home/user/dev/inv/gemi_invoice_backup2 COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_TARGET=lib/main.dart FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NAME=1.0.1 FLUTTER_BUILD_NUMBER=1 EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 4a5dc51..3d687cb 100755 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -5,7 +5,7 @@ export "FLUTTER_APPLICATION_PATH=/home/user/dev/inv/gemi_invoice_backup2" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NAME=1.0.1" export "FLUTTER_BUILD_NUMBER=1" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" diff --git a/lib/screens/customer_master_screen.dart b/lib/screens/customer_master_screen.dart new file mode 100644 index 0000000..cac66f0 --- /dev/null +++ b/lib/screens/customer_master_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; +import '../models/customer_model.dart'; +import '../services/customer_repository.dart'; + +class CustomerMasterScreen extends StatefulWidget { + const CustomerMasterScreen({Key? key}) : super(key: key); + + @override + State createState() => _CustomerMasterScreenState(); +} + +class _CustomerMasterScreenState extends State { + final CustomerRepository _customerRepo = CustomerRepository(); + List _customers = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadCustomers(); + } + + Future _loadCustomers() async { + setState(() => _isLoading = true); + final customers = await _customerRepo.getAllCustomers(); + setState(() { + _customers = customers; + _isLoading = false; + }); + } + + Future _addOrEditCustomer({Customer? customer}) async { + final isEdit = customer != null; + final displayNameController = TextEditingController(text: customer?.displayName ?? ""); + final formalNameController = TextEditingController(text: customer?.formalName ?? ""); + final departmentController = TextEditingController(text: customer?.department ?? ""); + final addressController = TextEditingController(text: customer?.address ?? ""); + final telController = TextEditingController(text: customer?.tel ?? ""); + String selectedTitle = customer?.title ?? "様"; + + final result = await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(isEdit ? "顧客を編集" : "顧客を新規登録"), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: displayNameController, + decoration: const InputDecoration(labelText: "表示名(略称)", hintText: "例: 佐々木製作所"), + ), + TextField( + controller: formalNameController, + decoration: const InputDecoration(labelText: "正式名称", hintText: "例: 株式会社 佐々木製作所"), + ), + DropdownButtonFormField( + value: selectedTitle, + decoration: const InputDecoration(labelText: "敬称"), + items: ["様", "御中", "殿", "貴社"].map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(), + onChanged: (val) => selectedTitle = val ?? "様", + ), + TextField( + controller: departmentController, + decoration: const InputDecoration(labelText: "部署名", hintText: "例: 営業部"), + ), + TextField( + controller: addressController, + decoration: const InputDecoration(labelText: "住所"), + ), + TextField( + controller: telController, + decoration: const InputDecoration(labelText: "電話番号"), + keyboardType: TextInputType.phone, + ), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")), + TextButton( + onPressed: () { + if (displayNameController.text.isEmpty || formalNameController.text.isEmpty) { + return; + } + final newCustomer = Customer( + id: customer?.id ?? const Uuid().v4(), + displayName: displayNameController.text, + formalName: formalNameController.text, + title: selectedTitle, + department: departmentController.text.isEmpty ? null : departmentController.text, + address: addressController.text.isEmpty ? null : addressController.text, + tel: telController.text.isEmpty ? null : telController.text, + odooId: customer?.odooId, + isSynced: false, + ); + Navigator.pop(context, newCustomer); + }, + child: const Text("保存"), + ), + ], + ), + ), + ); + + if (result != null) { + await _customerRepo.saveCustomer(result); + _loadCustomers(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("顧客マスター管理"), + backgroundColor: Colors.blueGrey, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _customers.isEmpty + ? const Center(child: Text("顧客が登録されていません")) + : ListView.builder( + itemCount: _customers.length, + itemBuilder: (context, index) { + final c = _customers[index]; + return ListTile( + title: Text(c.displayName), + subtitle: Text("${c.formalName} ${c.title}"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(icon: const Icon(Icons.edit), onPressed: () => _addOrEditCustomer(customer: c)), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("削除確認"), + content: Text("「${c.displayName}」を削除しますか?"), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")), + TextButton(onPressed: () => Navigator.pop(context, true), child: const Text("削除", style: TextStyle(color: Colors.red))), + ], + ), + ); + if (confirm == true) { + await _customerRepo.deleteCustomer(c.id); + _loadCustomers(); + } + }, + ), + ], + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _addOrEditCustomer(), + child: const Icon(Icons.person_add), + backgroundColor: Colors.indigo, + ), + ); + } +} diff --git a/lib/screens/invoice_detail_page.dart b/lib/screens/invoice_detail_page.dart index 799c557..999893a 100644 --- a/lib/screens/invoice_detail_page.dart +++ b/lib/screens/invoice_detail_page.dart @@ -10,8 +10,9 @@ import 'product_picker_modal.dart'; class InvoiceDetailPage extends StatefulWidget { final Invoice invoice; + final bool isUnlocked; - const InvoiceDetailPage({Key? key, required this.invoice}) : super(key: key); + const InvoiceDetailPage({Key? key, required this.invoice, this.isUnlocked = false}) : super(key: key); @override State createState() => _InvoiceDetailPageState(); @@ -137,7 +138,8 @@ class _InvoiceDetailPageState extends State { actions: [ if (!_isEditing) ...[ IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"), - IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)), + if (widget.isUnlocked) + IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)), ] else ...[ IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges), IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)), diff --git a/lib/screens/invoice_history_screen.dart b/lib/screens/invoice_history_screen.dart index 093f059..e7879b8 100644 --- a/lib/screens/invoice_history_screen.dart +++ b/lib/screens/invoice_history_screen.dart @@ -274,19 +274,16 @@ class _InvoiceHistoryScreenState extends State { ], ), onTap: () async { - if (!_isUnlocked) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("詳細の閲覧・編集にはアンロックが必要です"), duration: Duration(seconds: 1)), - ); - return; - } await Navigator.push( context, MaterialPageRoute( - builder: (context) => InvoiceDetailPage(invoice: invoice), + builder: (context) => InvoiceDetailPage( + invoice: invoice, + isUnlocked: _isUnlocked, // 状態を渡す + ), ), ); - _loadData(); // 戻ってきたら再読込 + _loadData(); }, onLongPress: () async { if (!_isUnlocked) { diff --git a/lib/screens/invoice_input_screen.dart b/lib/screens/invoice_input_screen.dart index db01ddf..aa831b6 100644 --- a/lib/screens/invoice_input_screen.dart +++ b/lib/screens/invoice_input_screen.dart @@ -64,7 +64,7 @@ class _InvoiceInputFormState extends State { int get _tax => _includeTax ? (_subTotal * _taxRate).round() : 0; int get _total => _subTotal + _tax; - Future _handleGenerate() async { + Future _saveInvoice({bool generatePdf = true}) async { if (_selectedCustomer == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("取引先を選択してください"))); return; @@ -79,19 +79,53 @@ class _InvoiceInputFormState extends State { date: DateTime.now(), items: _items, taxRate: _includeTax ? _taxRate : 0.0, - customerFormalNameSnapshot: _selectedCustomer!.formalName, // 追加 + customerFormalNameSnapshot: _selectedCustomer!.formalName, notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)", ); - setState(() => _status = "PDFを生成中..."); - final path = await generateInvoicePdf(invoice); - if (path != null) { - final updatedInvoice = invoice.copyWith(filePath: path); - await _repository.saveInvoice(updatedInvoice); - widget.onInvoiceGenerated(updatedInvoice, path); + if (generatePdf) { + setState(() => _status = "PDFを生成中..."); + final path = await generateInvoicePdf(invoice); + if (path != null) { + final updatedInvoice = invoice.copyWith(filePath: path); + await _repository.saveInvoice(updatedInvoice); + widget.onInvoiceGenerated(updatedInvoice, path); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存し、PDFを生成しました"))); + } + } else { + await _repository.saveInvoice(invoice); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を保存しました(PDF未生成)"))); + Navigator.pop(context); // 入力を閉じる } } + void _showPreview() { + if (_selectedCustomer == null) return; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("伝票プレビュー(仮)"), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("宛名: ${_selectedCustomer!.formalName} ${_selectedCustomer!.title}"), + const Divider(), + ..._items.map((it) => Text("・${it.description} x ${it.quantity} = ¥${it.subtotal}")), + const Divider(), + Text("小計: ¥${NumberFormat("#,###").format(_subTotal)}"), + Text("消費税: ¥${NumberFormat("#,###").format(_tax)}"), + Text("合計: ¥${NumberFormat("#,###").format(_total)}", style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text("閉じる")), + ], + ), + ); + } + @override Widget build(BuildContext context) { final fmt = NumberFormat("#,###"); @@ -295,20 +329,56 @@ class _InvoiceInputFormState extends State { Widget _buildBottomActionBar() { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), decoration: BoxDecoration( color: Colors.white, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, -5))], ), - child: ElevatedButton.icon( - onPressed: _handleGenerate, - icon: const Icon(Icons.picture_as_pdf), - label: const Text("伝票を確定してPDF生成"), - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 60), - backgroundColor: Colors.indigo, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _showPreview, + icon: const Icon(Icons.remove_red_eye), + label: const Text("仮表示"), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + side: const BorderSide(color: Colors.indigo), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _saveInvoice(generatePdf: false), + icon: const Icon(Icons.save), + label: const Text("保存のみ"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ElevatedButton.icon( + onPressed: () => _saveInvoice(generatePdf: true), + icon: const Icon(Icons.picture_as_pdf), + label: const Text("確定してPDF生成"), + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + backgroundColor: Colors.indigo, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + ], ), ), ); diff --git a/lib/screens/management_screen.dart b/lib/screens/management_screen.dart index 6ce9778..ec2b086 100644 --- a/lib/screens/management_screen.dart +++ b/lib/screens/management_screen.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; -import '../models/invoice_models.dart'; import '../services/invoice_repository.dart'; import '../services/customer_repository.dart'; import 'product_master_screen.dart'; +import 'customer_master_screen.dart'; class ManagementScreen extends StatelessWidget { const ManagementScreen({Key? key}) : super(key: key); @@ -28,6 +28,13 @@ class ManagementScreen extends StatelessWidget { "販売商品の名称や単価を管理します", () => Navigator.push(context, MaterialPageRoute(builder: (context) => const ProductMasterScreen())), ), + _buildMenuTile( + context, + Icons.people, + "顧客マスター管理", + "取引先(請求先)の名称や敬称を管理します", + () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CustomerMasterScreen())), + ), _buildMenuTile( context, Icons.upload_file, diff --git a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 0dd6f11..2e06f3d 100644 --- a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -3,7 +3,7 @@ FLUTTER_ROOT=/home/user/development/flutter FLUTTER_APPLICATION_PATH=/home/user/dev/inv/gemi_invoice_backup2 COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NAME=1.0.1 FLUTTER_BUILD_NUMBER=1 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true diff --git a/macos/Flutter/ephemeral/flutter_export_environment.sh b/macos/Flutter/ephemeral/flutter_export_environment.sh index 763f3c9..ba7ad8c 100755 --- a/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -4,7 +4,7 @@ export "FLUTTER_ROOT=/home/user/development/flutter" export "FLUTTER_APPLICATION_PATH=/home/user/dev/inv/gemi_invoice_backup2" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NAME=1.0.1" export "FLUTTER_BUILD_NUMBER=1" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" diff --git a/目標.md b/目標.md index 840bf7b..47e7e83 100644 --- a/目標.md +++ b/目標.md @@ -32,4 +32,7 @@ - 自社情報編集の画面で消費税を設定可能にする - 自社情報編集で印鑑を撮影出来る様にする - 商品マスター等でバーコードQRコードのスキャンが可能でありたい + − ロック機能はご動作対策であって削除と編集機能以外は全部使える様にする + - 顧客マスターの新規・編集・削除機能を実装する + - PDF作成と保存と仮表示は別ボタンで実装