feat: Introduce invoice preview, separate save and PDF generation options, and gate invoice detail editing by unlock status, updating the build version to 1.0.1.
This commit is contained in:
parent
4bc682f887
commit
f98fed8c44
10 changed files with 280 additions and 33 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
168
lib/screens/customer_master_screen.dart
Normal file
168
lib/screens/customer_master_screen.dart
Normal file
|
|
@ -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<CustomerMasterScreen> createState() => _CustomerMasterScreenState();
|
||||
}
|
||||
|
||||
class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
||||
final CustomerRepository _customerRepo = CustomerRepository();
|
||||
List<Customer> _customers = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCustomers();
|
||||
}
|
||||
|
||||
Future<void> _loadCustomers() async {
|
||||
setState(() => _isLoading = true);
|
||||
final customers = await _customerRepo.getAllCustomers();
|
||||
setState(() {
|
||||
_customers = customers;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _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<Customer>(
|
||||
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<String>(
|
||||
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<bool>(
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
|
||||
|
|
@ -137,7 +138,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
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)),
|
||||
|
|
|
|||
|
|
@ -274,19 +274,16 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
],
|
||||
),
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
int get _tax => _includeTax ? (_subTotal * _taxRate).round() : 0;
|
||||
int get _total => _subTotal + _tax;
|
||||
|
||||
Future<void> _handleGenerate() async {
|
||||
Future<void> _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<InvoiceInputForm> {
|
|||
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<InvoiceInputForm> {
|
|||
|
||||
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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
3
目標.md
3
目標.md
|
|
@ -32,4 +32,7 @@
|
|||
- 自社情報編集の画面で消費税を設定可能にする
|
||||
- 自社情報編集で印鑑を撮影出来る様にする
|
||||
- 商品マスター等でバーコードQRコードのスキャンが可能でありたい
|
||||
− ロック機能はご動作対策であって削除と編集機能以外は全部使える様にする
|
||||
- 顧客マスターの新規・編集・削除機能を実装する
|
||||
- PDF作成と保存と仮表示は別ボタンで実装
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue