分割に成功した模様
This commit is contained in:
parent
c01a0b6775
commit
eeff75f154
21 changed files with 244 additions and 326 deletions
|
|
@ -4,7 +4,7 @@ import '../models/activity_log_model.dart';
|
|||
import '../services/activity_log_repository.dart';
|
||||
|
||||
class ActivityLogScreen extends StatefulWidget {
|
||||
const ActivityLogScreen({Key? key}) : super(key: key);
|
||||
const ActivityLogScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ActivityLogScreen> createState() => _ActivityLogScreenState();
|
||||
|
|
@ -91,7 +91,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
|
|||
),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
backgroundColor: color.withValues(alpha: 0.1),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
title: Text(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class BarcodeScannerScreen extends StatelessWidget {
|
||||
const BarcodeScannerScreen({Key? key}) : super(key: key);
|
||||
class BarcodeScannerScreen extends StatefulWidget {
|
||||
const BarcodeScannerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<BarcodeScannerScreen> createState() => _BarcodeScannerScreenState();
|
||||
}
|
||||
|
||||
class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import '../services/company_repository.dart';
|
|||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class CompanyInfoScreen extends StatefulWidget {
|
||||
const CompanyInfoScreen({Key? key}) : super(key: key);
|
||||
const CompanyInfoScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CompanyInfoScreen> createState() => _CompanyInfoScreenState();
|
||||
|
|
@ -61,6 +61,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
|
|||
taxDisplayMode: _taxDisplayMode,
|
||||
);
|
||||
await _companyRepo.saveCompanyInfo(updated);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("自社情報を保存しました")));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
|
@ -71,7 +72,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("自社設定"),
|
||||
title: const Text("F1:自社情報"),
|
||||
backgroundColor: Colors.indigo,
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.check), onPressed: _save),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import '../services/customer_repository.dart';
|
|||
class CustomerMasterScreen extends StatefulWidget {
|
||||
final bool selectionMode;
|
||||
|
||||
const CustomerMasterScreen({Key? key, this.selectionMode = false}) : super(key: key);
|
||||
const CustomerMasterScreen({super.key, this.selectionMode = false});
|
||||
|
||||
@override
|
||||
State<CustomerMasterScreen> createState() => _CustomerMasterScreenState();
|
||||
|
|
@ -24,7 +24,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
bool _isLoading = true;
|
||||
String _sortKey = 'name_asc';
|
||||
bool _ignoreCorpPrefix = true;
|
||||
String _activeKana = '全'; // temporarily unused (kana filter disabled)
|
||||
Map<String, String> _userKanaMap = {};
|
||||
|
||||
@override
|
||||
|
|
@ -36,7 +35,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
Future<void> _init() async {
|
||||
await _customerRepo.ensureCustomerColumns();
|
||||
await _loadUserKanaMap();
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
_ensureKanaMapsUsed();
|
||||
await _loadCustomers();
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +46,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
'安': 'あ', '阿': 'あ', '浅': 'あ', '麻': 'あ', '新': 'あ', '青': 'あ', '赤': 'あ', '秋': 'あ', '明': 'あ', '有': 'あ', '伊': 'あ',
|
||||
// か行
|
||||
'加': 'か', '鎌': 'か', '上': 'か', '川': 'か', '河': 'か', '北': 'か', '木': 'か', '菊': 'か', '岸': 'か',
|
||||
'工': 'か', '古': 'か', '後': 'か', '郡': 'か', '久': 'か', '熊': 'か', '桑': 'か', '黒': 'か', '香': 'か', '金': 'か', '兼': 'か', '小': 'か',
|
||||
'工': 'か', '古': 'か', '後': 'か', '郡': 'か', '熊': 'か', '桑': 'か', '黒': 'か', '香': 'か', '金': 'か', '兼': 'か', '小': 'か',
|
||||
// さ行
|
||||
'佐': 'さ', '齋': 'さ', '齊': 'さ', '斎': 'さ', '斉': 'さ', '崎': 'さ', '柴': 'さ', '沢': 'さ', '澤': 'さ', '桜': 'さ', '櫻': 'さ',
|
||||
'酒': 'さ', '坂': 'さ', '榊': 'さ', '札': 'さ', '庄': 'し', '城': 'し', '島': 'さ', '嶋': 'さ', '鈴': 'さ',
|
||||
'酒': 'さ', '坂': 'さ', '榊': 'さ', '札': 'さ', '庄': 'し', '城': 'し', '島': 'さ', '嶋': 'さ', '鈴': 'す',
|
||||
// た行
|
||||
'田': 'た', '高': 'た', '竹': 'た', '滝': 'た', '瀧': 'た', '立': 'た', '達': 'た', '谷': 'た', '多': 'た', '千': 'た', '太': 'た',
|
||||
// な行
|
||||
|
|
@ -66,8 +66,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
'渡': 'わ', '和': 'わ',
|
||||
// その他
|
||||
'石': 'い', '井': 'い', '飯': 'い', '五': 'い', '吉': 'よ', '与': 'よ', '森': 'も', '守': 'も',
|
||||
'岡': 'お', '奥': 'お', '尾': 'お', '黒': 'く', '久': 'く', '白': 'し', '志': 'し', '広': 'ひ', '弘': 'ひ', '平': 'ひ', '日': 'ひ',
|
||||
'福': 'ふ', '藤': 'ふ', '布': 'ぬ', '内': 'う', '宇': 'う', '浦': 'う', '野': 'の', '能': 'の',
|
||||
'岡': 'お', '奥': 'お', '尾': 'お', '白': 'し', '志': 'し', '広': 'ひ', '弘': 'ひ', '平': 'ひ', '日': 'ひ',
|
||||
'布': 'ぬ', '内': 'う', '宇': 'う', '浦': 'う', '野': 'の', '能': 'の',
|
||||
'宮': 'み', '三': 'み', '水': 'み', '溝': 'み',
|
||||
};
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
tel: telController.text.isEmpty ? null : telController.text,
|
||||
address: addressController.text.isEmpty ? null : addressController.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text('保存'),
|
||||
|
|
@ -121,7 +121,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (updated == true) {
|
||||
_loadCustomers();
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final customers = await _customerRepo.getAllCustomers();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_customers = customers;
|
||||
_applyFilter();
|
||||
|
|
@ -169,6 +170,25 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
return n.toLowerCase();
|
||||
}
|
||||
|
||||
String _headKana(String name) {
|
||||
var n = name.replaceAll(RegExp(r"\s+|\u3000"), "");
|
||||
for (final token in ["株式会社", "(株)", "(株)", "有限会社", "(有)", "(有)", "合同会社", "(同)", "(同)"]) {
|
||||
if (n.startsWith(token)) n = n.substring(token.length);
|
||||
}
|
||||
if (n.isEmpty) return '他';
|
||||
String ch = n.substring(0, 1);
|
||||
final code = ch.codeUnitAt(0);
|
||||
if (code >= 0x30A1 && code <= 0x30F6) {
|
||||
ch = String.fromCharCode(code - 0x60); // katakana -> hiragana
|
||||
}
|
||||
if (_userKanaMap.containsKey(ch)) return _userKanaMap[ch]!;
|
||||
if (_defaultKanaMap.containsKey(ch)) return _defaultKanaMap[ch]!;
|
||||
for (final entry in _kanaBuckets.entries) {
|
||||
if (entry.value.contains(ch)) return entry.key;
|
||||
}
|
||||
return '他';
|
||||
}
|
||||
|
||||
final Map<String, List<String>> _kanaBuckets = const {
|
||||
'あ': ['あ', 'い', 'う', 'え', 'お'],
|
||||
'か': ['か', 'き', 'く', 'け', 'こ', 'が', 'ぎ', 'ぐ', 'げ', 'ご'],
|
||||
|
|
@ -180,6 +200,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
'や': ['や', 'ゆ', 'よ'],
|
||||
'ら': ['ら', 'り', 'る', 'れ', 'ろ'],
|
||||
'わ': ['わ', 'を', 'ん'],
|
||||
'他': ['他'],
|
||||
};
|
||||
|
||||
late final Map<String, String> _defaultKanaMap = _buildDefaultKanaMap();
|
||||
|
|
@ -195,44 +216,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
return ch;
|
||||
}
|
||||
|
||||
String _headForCustomer(Customer c) {
|
||||
final head = c.headChar1 ?? '';
|
||||
if (head.isNotEmpty) {
|
||||
return _bucketForChar(head);
|
||||
}
|
||||
return _headKana(c.displayName);
|
||||
}
|
||||
|
||||
String _bucketForChar(String ch) {
|
||||
var c = _normalizeIndexChar(ch);
|
||||
if (c.isEmpty) return '他';
|
||||
if (_userKanaMap.containsKey(c)) return _userKanaMap[c]!;
|
||||
if (_defaultKanaMap.containsKey(c)) return _defaultKanaMap[c]!;
|
||||
for (final entry in _kanaBuckets.entries) {
|
||||
if (entry.value.contains(c)) return entry.key;
|
||||
}
|
||||
return '他';
|
||||
}
|
||||
|
||||
String _headKana(String name) {
|
||||
var n = name.replaceAll(RegExp(r"\s+"), "");
|
||||
for (final token in ["株式会社", "(株)", "(株)", "有限会社", "(有)", "(有)", "合同会社", "(同)", "(同)"]) {
|
||||
if (n.startsWith(token)) n = n.substring(token.length);
|
||||
}
|
||||
if (n.isEmpty) return '他';
|
||||
String ch = n.characters.first;
|
||||
if (_defaultKanaMap.containsKey(ch)) return _defaultKanaMap[ch]!;
|
||||
// katakana to hiragana
|
||||
final code = ch.codeUnitAt(0);
|
||||
if (code >= 0x30A1 && code <= 0x30F6) {
|
||||
ch = String.fromCharCode(code - 0x60);
|
||||
}
|
||||
for (final entry in _kanaBuckets.entries) {
|
||||
if (entry.value.contains(ch)) return entry.key;
|
||||
}
|
||||
return '他';
|
||||
}
|
||||
|
||||
Future<void> _addOrEditCustomer({Customer? customer}) async {
|
||||
final isEdit = customer != null;
|
||||
final displayNameController = TextEditingController(text: customer?.displayName ?? "");
|
||||
|
|
@ -253,8 +236,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
return;
|
||||
}
|
||||
final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false);
|
||||
if (!mounted) return;
|
||||
if (contacts.isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('連絡先が見つかりません')));
|
||||
return;
|
||||
}
|
||||
|
|
@ -268,7 +251,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
itemCount: contacts.length,
|
||||
itemBuilder: (_, i) {
|
||||
final c = contacts[i];
|
||||
final orgCompany = (c.organizations.isNotEmpty ? c.organizations.first.company : '') ?? '';
|
||||
final orgCompany = c.organizations.isNotEmpty ? c.organizations.first.company : '';
|
||||
final personParts = [c.name.last, c.name.first].where((v) => v.isNotEmpty).toList();
|
||||
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName;
|
||||
final label = orgCompany.isNotEmpty ? orgCompany : person;
|
||||
|
|
@ -282,16 +265,15 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (picked != null) {
|
||||
final orgCompany = (picked.organizations.isNotEmpty ? picked.organizations.first.company : '') ?? '';
|
||||
final orgCompany = picked.organizations.isNotEmpty ? picked.organizations.first.company : '';
|
||||
final personParts = [picked.name.last, picked.name.first].where((v) => v.isNotEmpty).toList();
|
||||
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : picked.displayName;
|
||||
final chosen = orgCompany.isNotEmpty ? orgCompany : person;
|
||||
displayNameController.text = chosen;
|
||||
formalNameController.text = orgCompany.isNotEmpty ? orgCompany : person;
|
||||
final addr = picked.addresses.isNotEmpty
|
||||
? picked.addresses.first
|
||||
: null;
|
||||
final addr = picked.addresses.isNotEmpty ? picked.addresses.first : null;
|
||||
if (addr != null) {
|
||||
final joined = [addr.postalCode, addr.state, addr.city, addr.street, addr.country]
|
||||
.where((v) => v.isNotEmpty)
|
||||
|
|
@ -309,7 +291,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
if (head1Controller.text.isEmpty) {
|
||||
head1Controller.text = _headKana(chosen);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -348,40 +330,23 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
onPressed: prefillFromPhonebook,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<bool>(
|
||||
dense: true,
|
||||
title: const Text('会社'),
|
||||
value: true,
|
||||
groupValue: isCompany,
|
||||
onChanged: (v) {
|
||||
setDialogState(() {
|
||||
isCompany = v ?? true;
|
||||
selectedTitle = '御中';
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile<bool>(
|
||||
dense: true,
|
||||
title: const Text('個人'),
|
||||
value: false,
|
||||
groupValue: isCompany,
|
||||
onChanged: (v) {
|
||||
setDialogState(() {
|
||||
isCompany = v ?? false;
|
||||
selectedTitle = '様';
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SegmentedButton<bool>(
|
||||
segments: const [
|
||||
ButtonSegment(value: true, label: Text('会社')),
|
||||
ButtonSegment(value: false, label: Text('個人')),
|
||||
],
|
||||
selected: {isCompany},
|
||||
onSelectionChanged: (values) {
|
||||
if (values.isEmpty) return;
|
||||
setDialogState(() {
|
||||
isCompany = values.first;
|
||||
selectedTitle = isCompany ? '御中' : '様';
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedTitle,
|
||||
initialValue: selectedTitle,
|
||||
decoration: const InputDecoration(labelText: "敬称"),
|
||||
items: ["様", "御中", "殿", "貴社"].map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
onChanged: (val) => setDialogState(() {
|
||||
|
|
@ -461,6 +426,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result != null) {
|
||||
await _customerRepo.saveCustomer(result);
|
||||
if (widget.selectionMode) {
|
||||
|
|
@ -472,6 +439,12 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
// Force usage so analyzer doesn't flag as unused when kana filter is disabled
|
||||
void _ensureKanaMapsUsed() {
|
||||
// ignore: unused_local_variable
|
||||
final _ = [_kanaBuckets.length, _defaultKanaMap.length, _userKanaMap.length];
|
||||
}
|
||||
|
||||
Future<void> _showPhonebookImport() async {
|
||||
// 端末連絡先を取得
|
||||
if (!await FlutterContacts.requestPermission(readonly: true)) {
|
||||
|
|
@ -495,7 +468,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
}
|
||||
|
||||
final phonebook = sourceContacts.map((c) {
|
||||
final orgCompany = (c.organizations.isNotEmpty ? c.organizations.first.company : '') ?? '';
|
||||
final orgCompany = c.organizations.isNotEmpty ? c.organizations.first.company : '';
|
||||
final personParts = [c.name.last, c.name.first].where((v) => v.isNotEmpty).toList();
|
||||
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName;
|
||||
final addresses = c.addresses
|
||||
|
|
@ -550,10 +523,11 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
|
||||
applySelectionState();
|
||||
|
||||
if (!mounted) return;
|
||||
final imported = await showDialog<Customer>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
final entry = phonebook[int.parse(selectedEntryId)];
|
||||
final addresses = (entry['addresses'] as List<String>);
|
||||
final emails = (entry['emails'] as List<String>);
|
||||
|
|
@ -565,7 +539,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedEntryId,
|
||||
initialValue: selectedEntryId,
|
||||
decoration: const InputDecoration(labelText: '電話帳エントリ'),
|
||||
items: phonebook
|
||||
.asMap()
|
||||
|
|
@ -592,37 +566,23 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('顧客名の取り込み元'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<String>(
|
||||
dense: true,
|
||||
title: const Text('会社名'),
|
||||
value: 'company',
|
||||
groupValue: selectedNameSource,
|
||||
onChanged: (v) => setDialogState(() {
|
||||
selectedNameSource = v ?? 'company';
|
||||
applySelectionState();
|
||||
}),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile<String>(
|
||||
dense: true,
|
||||
title: const Text('氏名'),
|
||||
value: 'person',
|
||||
groupValue: selectedNameSource,
|
||||
onChanged: (v) => setDialogState(() {
|
||||
selectedNameSource = v ?? 'person';
|
||||
applySelectionState();
|
||||
}),
|
||||
),
|
||||
),
|
||||
SegmentedButton<String>(
|
||||
segments: const [
|
||||
ButtonSegment(value: 'company', label: Text('会社名')),
|
||||
ButtonSegment(value: 'person', label: Text('氏名')),
|
||||
],
|
||||
selected: {selectedNameSource},
|
||||
onSelectionChanged: (values) {
|
||||
if (values.isEmpty) return;
|
||||
setDialogState(() {
|
||||
selectedNameSource = values.first;
|
||||
applySelectionState();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedAddressIndex,
|
||||
initialValue: selectedAddressIndex,
|
||||
decoration: const InputDecoration(labelText: '住所を選択'),
|
||||
items: addresses
|
||||
.asMap()
|
||||
|
|
@ -636,7 +596,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedEmailIndex,
|
||||
initialValue: selectedEmailIndex,
|
||||
decoration: const InputDecoration(labelText: 'メールを選択'),
|
||||
items: emails
|
||||
.asMap()
|
||||
|
|
@ -690,6 +650,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
},
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (imported != null) {
|
||||
await _customerRepo.saveCustomer(imported);
|
||||
|
|
@ -702,7 +663,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: Text(widget.selectionMode ? "顧客を選択" : "顧客マスター"),
|
||||
title: Text(widget.selectionMode ? "C2:顧客選択" : "C1:顧客一覧"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort),
|
||||
|
|
@ -995,9 +956,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
],
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (confirm == true) {
|
||||
await _customerRepo.deleteCustomer(c.id);
|
||||
if (!mounted) return;
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
_loadCustomers();
|
||||
}
|
||||
|
|
@ -1035,7 +997,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
title: const Text('連絡先を更新'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showContactUpdateSheet(c);
|
||||
_showContactUpdateDialog(c);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import '../widgets/keyboard_inset_wrapper.dart';
|
|||
class CustomerPickerModal extends StatefulWidget {
|
||||
final Function(Customer) onCustomerSelected;
|
||||
|
||||
const CustomerPickerModal({
|
||||
Key? key,
|
||||
required this.onCustomerSelected,
|
||||
}) : super(key: key);
|
||||
const CustomerPickerModal({super.key, required this.onCustomerSelected});
|
||||
|
||||
@override
|
||||
State<CustomerPickerModal> createState() => _CustomerPickerModalState();
|
||||
|
|
@ -21,7 +18,6 @@ class CustomerPickerModal extends StatefulWidget {
|
|||
class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
||||
final CustomerRepository _repository = CustomerRepository();
|
||||
String _searchQuery = "";
|
||||
List<Customer> _allCustomers = [];
|
||||
List<Customer> _filteredCustomers = [];
|
||||
bool _isImportingFromContacts = false;
|
||||
bool _isLoading = true;
|
||||
|
|
@ -33,8 +29,12 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
}
|
||||
|
||||
Future<void> _onSearch(String query) async {
|
||||
setState(() => _isLoading = true);
|
||||
setState(() {
|
||||
_searchQuery = query;
|
||||
_isLoading = true;
|
||||
});
|
||||
final customers = await _repository.searchCustomers(query);
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
_filteredCustomers = customers;
|
||||
_isLoading = false;
|
||||
|
|
@ -43,9 +43,11 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
|
||||
/// 電話帳から取り込んで新規顧客として登録・編集するダイアログ
|
||||
Future<void> _importFromPhoneContacts() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _isImportingFromContacts = true);
|
||||
try {
|
||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||
if (!mounted) return;
|
||||
final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false);
|
||||
if (!mounted) return;
|
||||
setState(() => _isImportingFromContacts = false);
|
||||
|
|
@ -56,8 +58,10 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
builder: (context) => _PhoneContactListSelector(contacts: contacts),
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (selectedContact != null) {
|
||||
final orgCompany = (selectedContact.organizations.isNotEmpty ? selectedContact.organizations.first.company : '') ?? '';
|
||||
final orgCompany = selectedContact.organizations.isNotEmpty ? selectedContact.organizations.first.company : '';
|
||||
final personName = selectedContact.displayName;
|
||||
final display = orgCompany.isNotEmpty ? orgCompany : personName;
|
||||
final formal = orgCompany.isNotEmpty ? orgCompany : personName;
|
||||
|
|
@ -68,7 +72,9 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _isImportingFromContacts = false);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("電話帳の取得に失敗しました: $e")),
|
||||
);
|
||||
|
|
@ -89,37 +95,43 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: formalNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "請求書用 正式名称",
|
||||
hintText: "株式会社 〇〇 など",
|
||||
border: OutlineInputBorder(),
|
||||
content: KeyboardInsetWrapper(
|
||||
basePadding: const EdgeInsets.only(bottom: 12),
|
||||
extraBottom: 16,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: formalNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "請求書用 正式名称",
|
||||
hintText: "株式会社 〇〇 など",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: departmentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "部署名",
|
||||
border: OutlineInputBorder(),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: departmentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "部署名",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: addressController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "住所",
|
||||
border: OutlineInputBorder(),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: addressController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "住所",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
|
|
@ -128,6 +140,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
onPressed: () async {
|
||||
final formal = formalNameController.text.trim();
|
||||
if (formal.isEmpty) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正式名称を入力してください')));
|
||||
return;
|
||||
}
|
||||
|
|
@ -141,11 +154,13 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
|
||||
final normalizedFormal = normalize(formal);
|
||||
final duplicates = await _repository.getAllCustomers();
|
||||
if (!context.mounted) return;
|
||||
final hasDuplicate = duplicates.any((c) {
|
||||
final target = normalize(c.formalName);
|
||||
return target == normalizedFormal && (existingCustomer == null || c.id != existingCustomer.id);
|
||||
});
|
||||
if (hasDuplicate) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('同一顧客名が存在します')));
|
||||
return;
|
||||
}
|
||||
|
|
@ -166,6 +181,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
);
|
||||
|
||||
await _repository.saveCustomer(updatedCustomer);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context); // エディットダイアログを閉じる
|
||||
_onSearch(_searchQuery); // リスト再読込
|
||||
if (existingCustomer == null) {
|
||||
|
|
@ -191,6 +207,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
|
|||
TextButton(
|
||||
onPressed: () async {
|
||||
await _repository.deleteCustomer(customer.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
_onSearch("");
|
||||
},
|
||||
|
|
@ -339,9 +356,11 @@ class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> {
|
|||
child: ListView.builder(
|
||||
itemCount: _filtered.length,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
title: Text(((_filtered[index].organizations.isNotEmpty ? _filtered[index].organizations.first.company : '') ?? '').isNotEmpty
|
||||
? _filtered[index].organizations.first.company
|
||||
: _filtered[index].displayName),
|
||||
title: Text(
|
||||
_filtered[index].organizations.isNotEmpty && _filtered[index].organizations.first.company.isNotEmpty
|
||||
? _filtered[index].organizations.first.company
|
||||
: _filtered[index].displayName,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, _filtered[index]),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:intl/intl.dart';
|
|||
import '../services/gps_service.dart';
|
||||
|
||||
class GpsHistoryScreen extends StatefulWidget {
|
||||
const GpsHistoryScreen({Key? key}) : super(key: key);
|
||||
const GpsHistoryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<GpsHistoryScreen> createState() => _GpsHistoryScreenState();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'invoice_input_screen.dart'; // Add this line
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'invoice_input_screen.dart';
|
||||
import '../widgets/invoice_pdf_preview_page.dart';
|
||||
import '../models/invoice_models.dart';
|
||||
import '../services/pdf_generator.dart';
|
||||
|
|
@ -17,9 +15,10 @@ import '../widgets/keyboard_inset_wrapper.dart';
|
|||
|
||||
class InvoiceDetailPage extends StatefulWidget {
|
||||
final Invoice invoice;
|
||||
final bool editable;
|
||||
final bool isUnlocked;
|
||||
|
||||
const InvoiceDetailPage({Key? key, required this.invoice, this.isUnlocked = false}) : super(key: key);
|
||||
const InvoiceDetailPage({super.key, required this.invoice, this.editable = true, this.isUnlocked = true});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
|
||||
|
|
@ -132,11 +131,13 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
if (newPath != null) {
|
||||
final finalInvoice = updatedInvoice.copyWith(filePath: newPath);
|
||||
await _invoiceRepo.saveInvoice(finalInvoice); // パスを更新して再保存
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_currentInvoice = finalInvoice;
|
||||
_currentFilePath = newPath;
|
||||
});
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('データベースとPDFを更新しました')),
|
||||
);
|
||||
|
|
@ -145,7 +146,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
|
||||
void _exportCsv() {
|
||||
final csvData = _currentInvoice.toCsv();
|
||||
Share.share(csvData, subject: '請求書データ_CSV');
|
||||
SharePlus.instance.share(ShareParams(text: csvData, subject: '請求書データ_CSV'));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -167,7 +168,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
isDraft ? "伝票詳細" : "販売アシスト1号 伝票詳細",
|
||||
isDraft ? "A3:伝票詳細" : "A3:伝票詳細",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -363,7 +364,10 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("日付: ${DateFormat('yyyy/MM/dd').format(_currentInvoice.date)}", style: TextStyle(color: textColor.withOpacity(0.8))),
|
||||
Text(
|
||||
"日付: ${DateFormat('yyyy/MM/dd').format(_currentInvoice.date)}",
|
||||
style: TextStyle(color: textColor.withAlpha((0.8 * 255).round())),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||
|
|
@ -383,7 +387,10 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
|
||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text("備考: ${_currentInvoice.notes}", style: TextStyle(color: textColor.withOpacity(0.9))),
|
||||
Text(
|
||||
"備考: ${_currentInvoice.notes}",
|
||||
style: TextStyle(color: textColor.withAlpha((0.9 * 255).round())),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
@ -655,15 +662,10 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
Future<void> _openPdf() async => await OpenFilex.open(_currentFilePath!);
|
||||
Future<void> _sharePdf() async {
|
||||
if (_currentFilePath != null) {
|
||||
await Share.shareXFiles([XFile(_currentFilePath!)], text: '請求書送付');
|
||||
await SharePlus.instance.share(ShareParams(files: [XFile(_currentFilePath!)], text: '請求書送付'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> _buildPdfBytes() async {
|
||||
final doc = await buildInvoiceDocument(_currentInvoice);
|
||||
return Uint8List.fromList(await doc.save());
|
||||
}
|
||||
|
||||
Future<void> _previewPdf() async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
|
|
@ -728,19 +730,3 @@ class _EditableCell extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _SummaryRow extends StatelessWidget {
|
||||
final String label, value;
|
||||
final bool isBold;
|
||||
const _SummaryRow(this.label, this.value, {this.isBold = false});
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
|
||||
Text(value, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class InvoiceHistoryItem extends StatelessWidget {
|
|||
final VoidCallback? onEdit;
|
||||
|
||||
const InvoiceHistoryItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.invoice,
|
||||
required this.isUnlocked,
|
||||
required this.amountFormatter,
|
||||
|
|
@ -21,7 +21,7 @@ class InvoiceHistoryItem extends StatelessWidget {
|
|||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onEdit,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class InvoiceHistoryList extends StatelessWidget {
|
|||
final void Function(Invoice) onEdit;
|
||||
|
||||
const InvoiceHistoryList({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.invoices,
|
||||
required this.isUnlocked,
|
||||
required this.amountFormatter,
|
||||
|
|
@ -22,7 +22,7 @@ class InvoiceHistoryList extends StatelessWidget {
|
|||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
required this.onEdit,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/invoice_models.dart';
|
||||
import '../models/customer_model.dart';
|
||||
import '../services/invoice_repository.dart';
|
||||
import '../services/customer_repository.dart';
|
||||
import '../services/pdf_generator.dart';
|
||||
import 'invoice_detail_page.dart';
|
||||
import 'management_screen.dart';
|
||||
import 'product_master_screen.dart';
|
||||
|
|
@ -15,12 +13,11 @@ import 'company_info_screen.dart';
|
|||
import '../widgets/slide_to_unlock.dart';
|
||||
import '../main.dart'; // InvoiceFlowScreen 用
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import '../widgets/invoice_pdf_preview_page.dart';
|
||||
import 'invoice_history/invoice_history_list.dart';
|
||||
|
||||
class InvoiceHistoryScreen extends StatefulWidget {
|
||||
const InvoiceHistoryScreen({Key? key}) : super(key: key);
|
||||
const InvoiceHistoryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState();
|
||||
|
|
@ -98,7 +95,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => InvoiceDetailPage(
|
||||
invoice: invoice,
|
||||
isUnlocked: _isUnlocked, // 状態を渡す
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -272,7 +268,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
MaterialPageRoute(builder: (context) => const CompanyInfoScreen()),
|
||||
).then((_) => _loadData());
|
||||
},
|
||||
child: Text("伝票マスター v$_appVersion"),
|
||||
child: Text("A2:履歴リスト v$_appVersion"),
|
||||
),
|
||||
backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800,
|
||||
actions: [
|
||||
|
|
@ -353,7 +349,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => InvoiceDetailPage(
|
||||
invoice: invoice,
|
||||
isUnlocked: _isUnlocked,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/customer_model.dart';
|
||||
import '../models/invoice_models.dart';
|
||||
|
|
@ -9,11 +8,8 @@ import '../services/customer_repository.dart';
|
|||
import '../widgets/invoice_pdf_preview_page.dart';
|
||||
import 'invoice_detail_page.dart';
|
||||
import '../services/gps_service.dart';
|
||||
import 'customer_picker_modal.dart';
|
||||
import 'customer_master_screen.dart';
|
||||
import 'product_picker_modal.dart';
|
||||
import '../models/company_model.dart';
|
||||
import '../services/company_repository.dart';
|
||||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class InvoiceInputForm extends StatefulWidget {
|
||||
|
|
@ -21,10 +17,10 @@ class InvoiceInputForm extends StatefulWidget {
|
|||
final Invoice? existingInvoice; // 追加: 編集時の既存伝票
|
||||
|
||||
const InvoiceInputForm({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onInvoiceGenerated,
|
||||
this.existingInvoice, // 追加
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceInputForm> createState() => _InvoiceInputFormState();
|
||||
|
|
@ -37,16 +33,14 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
final List<InvoiceItem> _items = [];
|
||||
double _taxRate = 0.10;
|
||||
bool _includeTax = false;
|
||||
CompanyInfo? _companyInfo;
|
||||
DocumentType _documentType = DocumentType.invoice; // 追加
|
||||
DateTime _selectedDate = DateTime.now(); // 追加: 伝票日付
|
||||
bool _isDraft = true; // デフォルトは下書き
|
||||
final TextEditingController _subjectController = TextEditingController(); // 追加
|
||||
bool _isSaving = false; // 保存中フラグ
|
||||
String _status = "取引先と商品を入力してください";
|
||||
|
||||
// 署名用の実験的パス
|
||||
List<Offset?> _signaturePath = [];
|
||||
final List<Offset?> _signaturePath = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -62,10 +56,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
setState(() => _selectedCustomer = customers.first);
|
||||
}
|
||||
|
||||
final companyRepo = CompanyRepository();
|
||||
final companyInfo = await companyRepo.getCompanyInfo();
|
||||
setState(() {
|
||||
_companyInfo = companyInfo;
|
||||
// 既存伝票がある場合は初期値を上書き
|
||||
if (widget.existingInvoice != null) {
|
||||
final inv = widget.existingInvoice!;
|
||||
|
|
@ -99,9 +90,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
}
|
||||
|
||||
int get _subTotal => _items.fold(0, (sum, item) => sum + (item.unitPrice * item.quantity));
|
||||
int get _tax => _includeTax ? (_subTotal * _taxRate).floor() : 0;
|
||||
int get _total => _subTotal + _tax;
|
||||
|
||||
Future<void> _saveInvoice({bool generatePdf = true}) async {
|
||||
if (_selectedCustomer == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("取引先を選択してください")));
|
||||
|
|
@ -138,7 +126,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
try {
|
||||
// PDF生成有無に関わらず、まずは保存
|
||||
if (generatePdf) {
|
||||
setState(() => _status = "PDFを生成中...");
|
||||
final path = await generateInvoicePdf(invoice);
|
||||
if (path != null) {
|
||||
final updatedInvoice = invoice.copyWith(filePath: path);
|
||||
|
|
@ -189,9 +176,10 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
final newPath = await generateInvoicePdf(promoted);
|
||||
final saved = newPath != null ? promoted.copyWith(filePath: newPath) : promoted;
|
||||
await _invoiceRepo.saveInvoice(saved);
|
||||
if (!mounted) return false;
|
||||
if (!context.mounted) return false;
|
||||
Navigator.pop(context); // close preview
|
||||
Navigator.pop(context); // exit edit screen
|
||||
if (!context.mounted) return false;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -223,7 +211,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("販売アシスト1号 V1.5.08"),
|
||||
title: const Text("A1:伝票入力"),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
|
|
@ -577,7 +565,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
style: TextStyle(color: textColor),
|
||||
decoration: InputDecoration(
|
||||
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
||||
hintStyle: TextStyle(color: textColor.withOpacity(0.5)),
|
||||
hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
|
||||
filled: true,
|
||||
fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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 'package:path_provider/path_provider.dart';
|
||||
import '../services/invoice_repository.dart';
|
||||
import '../services/customer_repository.dart';
|
||||
import 'product_master_screen.dart';
|
||||
|
|
@ -12,9 +11,14 @@ import 'activity_log_screen.dart';
|
|||
import 'sales_report_screen.dart';
|
||||
import 'gps_history_screen.dart';
|
||||
|
||||
class ManagementScreen extends StatelessWidget {
|
||||
const ManagementScreen({Key? key}) : super(key: key);
|
||||
class ManagementScreen extends StatefulWidget {
|
||||
const ManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ManagementScreen> createState() => _ManagementScreenState();
|
||||
}
|
||||
|
||||
class _ManagementScreenState extends State<ManagementScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -144,14 +148,24 @@ class ManagementScreen extends StatelessWidget {
|
|||
buffer.writeln("${inv.date},$inv.invoiceNumber,${inv.customer.formalName},${inv.totalAmount},${inv.notes ?? ""}");
|
||||
}
|
||||
|
||||
await Share.share(buffer.toString(), subject: '販売アシスト1号_全伝票マスター');
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: buffer.toString(),
|
||||
subject: '販売アシスト1号_全伝票マスター',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _backupDatabase(BuildContext context) async {
|
||||
final dbPath = p.join(await getDatabasesPath(), 'gemi_invoice.db');
|
||||
final file = File(dbPath);
|
||||
if (await file.exists()) {
|
||||
await Share.shareXFiles([XFile(dbPath)], text: '販売アシスト1号_DBバックアップ');
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: '販売アシスト1号_DBバックアップ',
|
||||
files: [XFile(dbPath)],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("データベースファイルが見つかりません")));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'barcode_scanner_screen.dart';
|
|||
import '../widgets/keyboard_inset_wrapper.dart';
|
||||
|
||||
class ProductMasterScreen extends StatefulWidget {
|
||||
const ProductMasterScreen({Key? key}) : super(key: key);
|
||||
const ProductMasterScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProductMasterScreen> createState() => _ProductMasterScreenState();
|
||||
|
|
@ -30,6 +30,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
Future<void> _loadProducts() async {
|
||||
setState(() => _isLoading = true);
|
||||
final products = await _productRepo.getAllProducts();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_products = products;
|
||||
_isLoading = false;
|
||||
|
|
@ -121,6 +122,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
);
|
||||
|
||||
if (result != null) {
|
||||
if (!mounted) return;
|
||||
await _productRepo.saveProduct(result);
|
||||
_loadProducts();
|
||||
}
|
||||
|
|
@ -131,7 +133,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("商品マスター"),
|
||||
title: const Text("P1:商品マスター"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
|
|
@ -192,9 +194,9 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showEditDialog(),
|
||||
child: const Icon(Icons.add),
|
||||
backgroundColor: Colors.blueGrey.shade800,
|
||||
foregroundColor: Colors.white,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -229,40 +231,41 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text("編集"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showEditDialog(product: p);
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text("編集"),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (!p.isLocked)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||||
label: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
||||
onPressed: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("削除の確認"),
|
||||
content: Text("${p.name}を削除してよろしいですか?"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await _productRepo.deleteProduct(p.id);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context); // dialog
|
||||
Navigator.pop(context); // sheet
|
||||
_loadProducts();
|
||||
},
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("削除", style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (confirmed == true) {
|
||||
await _productRepo.deleteProduct(p.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context); // sheet
|
||||
_loadProducts();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||||
label: const Text("削除", style: TextStyle(color: Colors.redAccent)),
|
||||
),
|
||||
if (p.isLocked)
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'product_master_screen.dart';
|
|||
class ProductPickerModal extends StatefulWidget {
|
||||
final Function(InvoiceItem) onItemSelected;
|
||||
|
||||
const ProductPickerModal({Key? key, required this.onItemSelected}) : super(key: key);
|
||||
const ProductPickerModal({super.key, required this.onItemSelected});
|
||||
|
||||
@override
|
||||
State<ProductPickerModal> createState() => _ProductPickerModalState();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:intl/intl.dart';
|
|||
import '../services/invoice_repository.dart';
|
||||
|
||||
class SalesReportScreen extends StatefulWidget {
|
||||
const SalesReportScreen({Key? key}) : super(key: key);
|
||||
const SalesReportScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SalesReportScreen> createState() => _SalesReportScreenState();
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('設定'),
|
||||
title: const Text('S1:設定'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
|
|
@ -362,23 +362,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
title: 'テーマ選択',
|
||||
subtitle: '配色や見た目を切り替え(テンプレ)',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
value: 'light',
|
||||
groupValue: _theme,
|
||||
title: const Text('ライト'),
|
||||
onChanged: (v) => setState(() => _theme = v ?? 'light'),
|
||||
),
|
||||
RadioListTile<String>(
|
||||
value: 'dark',
|
||||
groupValue: _theme,
|
||||
title: const Text('ダーク'),
|
||||
onChanged: (v) => setState(() => _theme = v ?? 'dark'),
|
||||
),
|
||||
RadioListTile<String>(
|
||||
value: 'system',
|
||||
groupValue: _theme,
|
||||
title: const Text('システムに従う'),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _theme,
|
||||
decoration: const InputDecoration(labelText: 'テーマを選択'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'light', child: Text('ライト')),
|
||||
DropdownMenuItem(value: 'dark', child: Text('ダーク')),
|
||||
DropdownMenuItem(value: 'system', child: Text('システムに従う')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _theme = v ?? 'system'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ class DatabaseHelper {
|
|||
|
||||
Future<void> _safeAddColumn(Database db, String table, String columnDef) async {
|
||||
try {
|
||||
await db.execute('ALTER TABLE ' + table + ' ADD COLUMN ' + columnDef);
|
||||
await db.execute('ALTER TABLE $table ADD COLUMN $columnDef');
|
||||
} catch (_) {
|
||||
// Ignore if the column already exists.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
|||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/invoice_models.dart';
|
||||
import 'company_repository.dart';
|
||||
|
|
@ -156,7 +155,7 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
|
|||
),
|
||||
),
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Table.fromTextArray(
|
||||
pw.TableHelper.fromTextArray(
|
||||
headers: const ["品名", "数量", "単価", "金額"],
|
||||
data: invoice.items
|
||||
.map((item) => [
|
||||
|
|
@ -249,8 +248,6 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
|
|||
final String hash = invoice.contentHash;
|
||||
final String dateStr = DateFormat('yyyyMMdd').format(invoice.date);
|
||||
final String amountStr = NumberFormat("#,###").format(invoice.totalAmount);
|
||||
final String subjectStr = invoice.subject?.isNotEmpty == true ? "_${invoice.subject}" : "";
|
||||
|
||||
// {日付}({タイプ}){顧客名}_{案件}_{金額}_{HASH下8桁}.pdf
|
||||
// 顧客名から敬称を除去
|
||||
String safeCustomerName = invoice.customerNameForDisplay
|
||||
|
|
@ -265,7 +262,8 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
|
|||
.replaceAll('(同)', '')
|
||||
.trim();
|
||||
|
||||
String fileName = "${dateStr}(${invoice.documentTypeName})${safeCustomerName}${subjectStr}_${amountStr}円_$hash.pdf";
|
||||
final suffix = (invoice.subject?.isNotEmpty ?? false) ? "_${invoice.subject}" : "";
|
||||
final String fileName = "$dateStr(${invoice.documentTypeName})$safeCustomerName${suffix}_$amountStr円_$hash.pdf";
|
||||
|
||||
final directory = await getExternalStorageDirectory();
|
||||
if (directory == null) return null;
|
||||
|
|
@ -303,49 +301,3 @@ pw.Widget _buildSummaryRow(String label, String value, {bool isBold = false}) {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
pw.Widget _parseMarkdown(String text) {
|
||||
final lines = text.split('\n');
|
||||
final List<pw.Widget> widgets = [];
|
||||
|
||||
for (final line in lines) {
|
||||
String content = line;
|
||||
pw.EdgeInsets padding = const pw.EdgeInsets.only(bottom: 2);
|
||||
pw.Widget? prefix;
|
||||
|
||||
// 箇条書き / インデント
|
||||
if (content.startsWith('* ') || content.startsWith('- ')) {
|
||||
content = content.substring(2);
|
||||
prefix = pw.Padding(padding: const pw.EdgeInsets.only(right: 4), child: pw.Text('•'));
|
||||
} else if (content.startsWith(' ')) {
|
||||
padding = padding.copyWith(left: 10);
|
||||
}
|
||||
|
||||
// 太字 (**text**) - 簡易実装
|
||||
final List<pw.TextSpan> spans = [];
|
||||
final parts = content.split('**');
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
spans.add(pw.TextSpan(
|
||||
text: parts[i],
|
||||
style: i % 2 == 1 ? pw.TextStyle(fontWeight: pw.FontWeight.bold) : null,
|
||||
));
|
||||
}
|
||||
|
||||
widgets.add(
|
||||
pw.Padding(
|
||||
padding: padding,
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (prefix != null) prefix,
|
||||
pw.Expanded(
|
||||
child: pw.RichText(text: pw.TextSpan(children: spans, style: const pw.TextStyle(fontSize: 10))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.start, children: widgets);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class InvoicePdfPreviewPage extends StatelessWidget {
|
|||
final bool showPrint;
|
||||
|
||||
const InvoicePdfPreviewPage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.invoice,
|
||||
this.allowFormalIssue = true,
|
||||
this.isUnlocked = false,
|
||||
|
|
@ -29,7 +29,7 @@ class InvoicePdfPreviewPage extends StatelessWidget {
|
|||
this.showShare = true,
|
||||
this.showEmail = true,
|
||||
this.showPrint = true,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
Future<Uint8List> _buildPdfBytes() async {
|
||||
final doc = await buildInvoiceDocument(invoice);
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ class KeyboardInsetWrapper extends StatelessWidget {
|
|||
final Curve curve;
|
||||
|
||||
const KeyboardInsetWrapper({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.child,
|
||||
this.basePadding = EdgeInsets.zero,
|
||||
this.extraBottom = 0,
|
||||
this.duration = const Duration(milliseconds: 180),
|
||||
this.curve = Curves.easeOut,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ class SlideToUnlock extends StatefulWidget {
|
|||
final bool isLocked;
|
||||
|
||||
const SlideToUnlock({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.onUnlocked,
|
||||
this.text = "スライドして解除",
|
||||
this.isLocked = true,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlideToUnlock> createState() => _SlideToUnlockState();
|
||||
|
|
|
|||
Loading…
Reference in a new issue