分割に成功した模様

This commit is contained in:
joe 2026-02-27 12:14:14 +09:00
parent c01a0b6775
commit eeff75f154
21 changed files with 244 additions and 326 deletions

View file

@ -4,7 +4,7 @@ import '../models/activity_log_model.dart';
import '../services/activity_log_repository.dart'; import '../services/activity_log_repository.dart';
class ActivityLogScreen extends StatefulWidget { class ActivityLogScreen extends StatefulWidget {
const ActivityLogScreen({Key? key}) : super(key: key); const ActivityLogScreen({super.key});
@override @override
State<ActivityLogScreen> createState() => _ActivityLogScreenState(); State<ActivityLogScreen> createState() => _ActivityLogScreenState();
@ -91,7 +91,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
), ),
child: ListTile( child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: color.withOpacity(0.1), backgroundColor: color.withValues(alpha: 0.1),
child: Icon(icon, color: color, size: 20), child: Icon(icon, color: color, size: 20),
), ),
title: Text( title: Text(

View file

@ -1,9 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerScreen extends StatelessWidget { class BarcodeScannerScreen extends StatefulWidget {
const BarcodeScannerScreen({Key? key}) : super(key: key); const BarcodeScannerScreen({super.key});
@override
State<BarcodeScannerScreen> createState() => _BarcodeScannerScreenState();
}
class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View file

@ -6,7 +6,7 @@ import '../services/company_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart'; import '../widgets/keyboard_inset_wrapper.dart';
class CompanyInfoScreen extends StatefulWidget { class CompanyInfoScreen extends StatefulWidget {
const CompanyInfoScreen({Key? key}) : super(key: key); const CompanyInfoScreen({super.key});
@override @override
State<CompanyInfoScreen> createState() => _CompanyInfoScreenState(); State<CompanyInfoScreen> createState() => _CompanyInfoScreenState();
@ -61,6 +61,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
taxDisplayMode: _taxDisplayMode, taxDisplayMode: _taxDisplayMode,
); );
await _companyRepo.saveCompanyInfo(updated); await _companyRepo.saveCompanyInfo(updated);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("自社情報を保存しました"))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("自社情報を保存しました")));
Navigator.pop(context); Navigator.pop(context);
} }
@ -71,7 +72,7 @@ class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("自社設定"), title: const Text("F1:自社情報"),
backgroundColor: Colors.indigo, backgroundColor: Colors.indigo,
actions: [ actions: [
IconButton(icon: const Icon(Icons.check), onPressed: _save), IconButton(icon: const Icon(Icons.check), onPressed: _save),

View file

@ -10,7 +10,7 @@ import '../services/customer_repository.dart';
class CustomerMasterScreen extends StatefulWidget { class CustomerMasterScreen extends StatefulWidget {
final bool selectionMode; final bool selectionMode;
const CustomerMasterScreen({Key? key, this.selectionMode = false}) : super(key: key); const CustomerMasterScreen({super.key, this.selectionMode = false});
@override @override
State<CustomerMasterScreen> createState() => _CustomerMasterScreenState(); State<CustomerMasterScreen> createState() => _CustomerMasterScreenState();
@ -24,7 +24,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
bool _isLoading = true; bool _isLoading = true;
String _sortKey = 'name_asc'; String _sortKey = 'name_asc';
bool _ignoreCorpPrefix = true; bool _ignoreCorpPrefix = true;
String _activeKana = ''; // temporarily unused (kana filter disabled)
Map<String, String> _userKanaMap = {}; Map<String, String> _userKanaMap = {};
@override @override
@ -36,7 +35,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
Future<void> _init() async { Future<void> _init() async {
await _customerRepo.ensureCustomerColumns(); await _customerRepo.ensureCustomerColumns();
await _loadUserKanaMap(); await _loadUserKanaMap();
if (!mounted) return; if (!context.mounted) return;
_ensureKanaMapsUsed();
await _loadCustomers(); 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, tel: telController.text.isEmpty ? null : telController.text,
address: addressController.text.isEmpty ? null : addressController.text, address: addressController.text.isEmpty ? null : addressController.text,
); );
if (!mounted) return; if (!context.mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
}, },
child: const Text('保存'), child: const Text('保存'),
@ -121,7 +121,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
], ],
), ),
); );
if (!mounted) return;
if (updated == true) { if (updated == true) {
_loadCustomers(); _loadCustomers();
} }
@ -131,6 +131,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final customers = await _customerRepo.getAllCustomers(); final customers = await _customerRepo.getAllCustomers();
if (!mounted) return;
setState(() { setState(() {
_customers = customers; _customers = customers;
_applyFilter(); _applyFilter();
@ -169,6 +170,25 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return n.toLowerCase(); 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 { final Map<String, List<String>> _kanaBuckets = const {
'': ['', '', '', '', ''], '': ['', '', '', '', ''],
'': ['', '', '', '', '', '', '', '', '', ''], '': ['', '', '', '', '', '', '', '', '', ''],
@ -180,6 +200,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
'': ['', '', ''], '': ['', '', ''],
'': ['', '', '', '', ''], '': ['', '', '', '', ''],
'': ['', '', ''], '': ['', '', ''],
'': [''],
}; };
late final Map<String, String> _defaultKanaMap = _buildDefaultKanaMap(); late final Map<String, String> _defaultKanaMap = _buildDefaultKanaMap();
@ -195,44 +216,6 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return ch; 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 { Future<void> _addOrEditCustomer({Customer? customer}) async {
final isEdit = customer != null; final isEdit = customer != null;
final displayNameController = TextEditingController(text: customer?.displayName ?? ""); final displayNameController = TextEditingController(text: customer?.displayName ?? "");
@ -253,8 +236,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return; return;
} }
final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false); final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false);
if (!mounted) return;
if (contacts.isEmpty) { if (contacts.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('連絡先が見つかりません'))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('連絡先が見つかりません')));
return; return;
} }
@ -268,7 +251,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
itemCount: contacts.length, itemCount: contacts.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final c = contacts[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 personParts = [c.name.last, c.name.first].where((v) => v.isNotEmpty).toList();
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName; final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName;
final label = orgCompany.isNotEmpty ? orgCompany : person; final label = orgCompany.isNotEmpty ? orgCompany : person;
@ -282,16 +265,15 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
), ),
); );
if (!mounted) return;
if (picked != null) { 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 personParts = [picked.name.last, picked.name.first].where((v) => v.isNotEmpty).toList();
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : picked.displayName; final person = personParts.isNotEmpty ? personParts.join(' ').trim() : picked.displayName;
final chosen = orgCompany.isNotEmpty ? orgCompany : person; final chosen = orgCompany.isNotEmpty ? orgCompany : person;
displayNameController.text = chosen; displayNameController.text = chosen;
formalNameController.text = orgCompany.isNotEmpty ? orgCompany : person; formalNameController.text = orgCompany.isNotEmpty ? orgCompany : person;
final addr = picked.addresses.isNotEmpty final addr = picked.addresses.isNotEmpty ? picked.addresses.first : null;
? picked.addresses.first
: null;
if (addr != null) { if (addr != null) {
final joined = [addr.postalCode, addr.state, addr.city, addr.street, addr.country] final joined = [addr.postalCode, addr.state, addr.city, addr.street, addr.country]
.where((v) => v.isNotEmpty) .where((v) => v.isNotEmpty)
@ -309,7 +291,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
if (head1Controller.text.isEmpty) { if (head1Controller.text.isEmpty) {
head1Controller.text = _headKana(chosen); head1Controller.text = _headKana(chosen);
} }
if (mounted) setState(() {}); setState(() {});
} }
} }
@ -348,40 +330,23 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
onPressed: prefillFromPhonebook, onPressed: prefillFromPhonebook,
), ),
), ),
Row( SegmentedButton<bool>(
children: [ segments: const [
Expanded( ButtonSegment(value: true, label: Text('会社')),
child: RadioListTile<bool>( ButtonSegment(value: false, label: Text('個人')),
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 = '';
});
},
),
),
], ],
selected: {isCompany},
onSelectionChanged: (values) {
if (values.isEmpty) return;
setDialogState(() {
isCompany = values.first;
selectedTitle = isCompany ? '御中' : '';
});
},
), ),
const SizedBox(height: 8),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedTitle, initialValue: selectedTitle,
decoration: const InputDecoration(labelText: "敬称"), decoration: const InputDecoration(labelText: "敬称"),
items: ["", "御中", "殿", "貴社"].map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(), items: ["", "御中", "殿", "貴社"].map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
onChanged: (val) => setDialogState(() { onChanged: (val) => setDialogState(() {
@ -461,6 +426,8 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
); );
if (!mounted) return;
if (result != null) { if (result != null) {
await _customerRepo.saveCustomer(result); await _customerRepo.saveCustomer(result);
if (widget.selectionMode) { 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 { Future<void> _showPhonebookImport() async {
// //
if (!await FlutterContacts.requestPermission(readonly: true)) { if (!await FlutterContacts.requestPermission(readonly: true)) {
@ -495,7 +468,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
} }
final phonebook = sourceContacts.map((c) { 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 personParts = [c.name.last, c.name.first].where((v) => v.isNotEmpty).toList();
final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName; final person = personParts.isNotEmpty ? personParts.join(' ').trim() : c.displayName;
final addresses = c.addresses final addresses = c.addresses
@ -550,10 +523,11 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
applySelectionState(); applySelectionState();
if (!mounted) return;
final imported = await showDialog<Customer>( final imported = await showDialog<Customer>(
context: context, context: context,
builder: (context) => StatefulBuilder( builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) { builder: (ctx, setDialogState) {
final entry = phonebook[int.parse(selectedEntryId)]; final entry = phonebook[int.parse(selectedEntryId)];
final addresses = (entry['addresses'] as List<String>); final addresses = (entry['addresses'] as List<String>);
final emails = (entry['emails'] as List<String>); final emails = (entry['emails'] as List<String>);
@ -565,7 +539,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedEntryId, initialValue: selectedEntryId,
decoration: const InputDecoration(labelText: '電話帳エントリ'), decoration: const InputDecoration(labelText: '電話帳エントリ'),
items: phonebook items: phonebook
.asMap() .asMap()
@ -592,37 +566,23 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('顧客名の取り込み元'), const Text('顧客名の取り込み元'),
Row( SegmentedButton<String>(
children: [ segments: const [
Expanded( ButtonSegment(value: 'company', label: Text('会社名')),
child: RadioListTile<String>( ButtonSegment(value: 'person', label: Text('氏名')),
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();
}),
),
),
], ],
selected: {selectedNameSource},
onSelectionChanged: (values) {
if (values.isEmpty) return;
setDialogState(() {
selectedNameSource = values.first;
applySelectionState();
});
},
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
value: selectedAddressIndex, initialValue: selectedAddressIndex,
decoration: const InputDecoration(labelText: '住所を選択'), decoration: const InputDecoration(labelText: '住所を選択'),
items: addresses items: addresses
.asMap() .asMap()
@ -636,7 +596,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
value: selectedEmailIndex, initialValue: selectedEmailIndex,
decoration: const InputDecoration(labelText: 'メールを選択'), decoration: const InputDecoration(labelText: 'メールを選択'),
items: emails items: emails
.asMap() .asMap()
@ -690,6 +650,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
}, },
), ),
); );
if (!context.mounted) return;
if (imported != null) { if (imported != null) {
await _customerRepo.saveCustomer(imported); await _customerRepo.saveCustomer(imported);
@ -702,7 +663,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), leading: const BackButton(),
title: Text(widget.selectionMode ? "顧客を選択" : "顧客マスター"), title: Text(widget.selectionMode ? "C2:顧客選択" : "C1:顧客一覧"),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.sort), icon: const Icon(Icons.sort),
@ -995,9 +956,10 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
], ],
), ),
); );
if (!context.mounted) return;
if (confirm == true) { if (confirm == true) {
await _customerRepo.deleteCustomer(c.id); await _customerRepo.deleteCustomer(c.id);
if (!mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
_loadCustomers(); _loadCustomers();
} }
@ -1035,7 +997,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
title: const Text('連絡先を更新'), title: const Text('連絡先を更新'),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
_showContactUpdateSheet(c); _showContactUpdateDialog(c);
}, },
), ),
ListTile( ListTile(

View file

@ -9,10 +9,7 @@ import '../widgets/keyboard_inset_wrapper.dart';
class CustomerPickerModal extends StatefulWidget { class CustomerPickerModal extends StatefulWidget {
final Function(Customer) onCustomerSelected; final Function(Customer) onCustomerSelected;
const CustomerPickerModal({ const CustomerPickerModal({super.key, required this.onCustomerSelected});
Key? key,
required this.onCustomerSelected,
}) : super(key: key);
@override @override
State<CustomerPickerModal> createState() => _CustomerPickerModalState(); State<CustomerPickerModal> createState() => _CustomerPickerModalState();
@ -21,7 +18,6 @@ class CustomerPickerModal extends StatefulWidget {
class _CustomerPickerModalState extends State<CustomerPickerModal> { class _CustomerPickerModalState extends State<CustomerPickerModal> {
final CustomerRepository _repository = CustomerRepository(); final CustomerRepository _repository = CustomerRepository();
String _searchQuery = ""; String _searchQuery = "";
List<Customer> _allCustomers = [];
List<Customer> _filteredCustomers = []; List<Customer> _filteredCustomers = [];
bool _isImportingFromContacts = false; bool _isImportingFromContacts = false;
bool _isLoading = true; bool _isLoading = true;
@ -33,8 +29,12 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
} }
Future<void> _onSearch(String query) async { Future<void> _onSearch(String query) async {
setState(() => _isLoading = true); setState(() {
_searchQuery = query;
_isLoading = true;
});
final customers = await _repository.searchCustomers(query); final customers = await _repository.searchCustomers(query);
if (!context.mounted) return;
setState(() { setState(() {
_filteredCustomers = customers; _filteredCustomers = customers;
_isLoading = false; _isLoading = false;
@ -43,9 +43,11 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
/// ///
Future<void> _importFromPhoneContacts() async { Future<void> _importFromPhoneContacts() async {
if (!mounted) return;
setState(() => _isImportingFromContacts = true); setState(() => _isImportingFromContacts = true);
try { try {
if (await FlutterContacts.requestPermission(readonly: true)) { if (await FlutterContacts.requestPermission(readonly: true)) {
if (!mounted) return;
final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false); final contacts = await FlutterContacts.getContacts(withProperties: true, withAccounts: true, withPhoto: false);
if (!mounted) return; if (!mounted) return;
setState(() => _isImportingFromContacts = false); setState(() => _isImportingFromContacts = false);
@ -56,8 +58,10 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
builder: (context) => _PhoneContactListSelector(contacts: contacts), builder: (context) => _PhoneContactListSelector(contacts: contacts),
); );
if (!context.mounted) return;
if (selectedContact != null) { 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 personName = selectedContact.displayName;
final display = orgCompany.isNotEmpty ? orgCompany : personName; final display = orgCompany.isNotEmpty ? orgCompany : personName;
final formal = orgCompany.isNotEmpty ? orgCompany : personName; final formal = orgCompany.isNotEmpty ? orgCompany : personName;
@ -68,7 +72,9 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
} }
} }
} catch (e) { } catch (e) {
if (!mounted) return;
setState(() => _isImportingFromContacts = false); setState(() => _isImportingFromContacts = false);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("電話帳の取得に失敗しました: $e")), SnackBar(content: Text("電話帳の取得に失敗しました: $e")),
); );
@ -89,37 +95,43 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"), title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"),
content: SingleChildScrollView( content: KeyboardInsetWrapper(
child: Column( basePadding: const EdgeInsets.only(bottom: 12),
mainAxisSize: MainAxisSize.min, extraBottom: 16,
children: [ child: SingleChildScrollView(
Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)), padding: const EdgeInsets.only(top: 4),
const SizedBox(height: 16), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
TextField( child: Column(
controller: formalNameController, mainAxisSize: MainAxisSize.min,
decoration: const InputDecoration( children: [
labelText: "請求書用 正式名称", Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)),
hintText: "株式会社 など", const SizedBox(height: 16),
border: OutlineInputBorder(), TextField(
controller: formalNameController,
decoration: const InputDecoration(
labelText: "請求書用 正式名称",
hintText: "株式会社 など",
border: OutlineInputBorder(),
),
), ),
), const SizedBox(height: 12),
const SizedBox(height: 12), TextField(
TextField( controller: departmentController,
controller: departmentController, decoration: const InputDecoration(
decoration: const InputDecoration( labelText: "部署名",
labelText: "部署名", border: OutlineInputBorder(),
border: OutlineInputBorder(), ),
), ),
), const SizedBox(height: 12),
const SizedBox(height: 12), TextField(
TextField( controller: addressController,
controller: addressController, decoration: const InputDecoration(
decoration: const InputDecoration( labelText: "住所",
labelText: "住所", border: OutlineInputBorder(),
border: OutlineInputBorder(), ),
), ),
), ],
], ),
), ),
), ),
actions: [ actions: [
@ -128,6 +140,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
onPressed: () async { onPressed: () async {
final formal = formalNameController.text.trim(); final formal = formalNameController.text.trim();
if (formal.isEmpty) { if (formal.isEmpty) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正式名称を入力してください'))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正式名称を入力してください')));
return; return;
} }
@ -141,11 +154,13 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
final normalizedFormal = normalize(formal); final normalizedFormal = normalize(formal);
final duplicates = await _repository.getAllCustomers(); final duplicates = await _repository.getAllCustomers();
if (!context.mounted) return;
final hasDuplicate = duplicates.any((c) { final hasDuplicate = duplicates.any((c) {
final target = normalize(c.formalName); final target = normalize(c.formalName);
return target == normalizedFormal && (existingCustomer == null || c.id != existingCustomer.id); return target == normalizedFormal && (existingCustomer == null || c.id != existingCustomer.id);
}); });
if (hasDuplicate) { if (hasDuplicate) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('同一顧客名が存在します'))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('同一顧客名が存在します')));
return; return;
} }
@ -166,6 +181,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
); );
await _repository.saveCustomer(updatedCustomer); await _repository.saveCustomer(updatedCustomer);
if (!context.mounted) return;
Navigator.pop(context); // Navigator.pop(context); //
_onSearch(_searchQuery); // _onSearch(_searchQuery); //
if (existingCustomer == null) { if (existingCustomer == null) {
@ -191,6 +207,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await _repository.deleteCustomer(customer.id); await _repository.deleteCustomer(customer.id);
if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
_onSearch(""); _onSearch("");
}, },
@ -339,9 +356,11 @@ class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> {
child: ListView.builder( child: ListView.builder(
itemCount: _filtered.length, itemCount: _filtered.length,
itemBuilder: (context, index) => ListTile( itemBuilder: (context, index) => ListTile(
title: Text(((_filtered[index].organizations.isNotEmpty ? _filtered[index].organizations.first.company : '') ?? '').isNotEmpty title: Text(
? _filtered[index].organizations.first.company _filtered[index].organizations.isNotEmpty && _filtered[index].organizations.first.company.isNotEmpty
: _filtered[index].displayName), ? _filtered[index].organizations.first.company
: _filtered[index].displayName,
),
onTap: () => Navigator.pop(context, _filtered[index]), onTap: () => Navigator.pop(context, _filtered[index]),
), ),
), ),

View file

@ -3,7 +3,7 @@ import 'package:intl/intl.dart';
import '../services/gps_service.dart'; import '../services/gps_service.dart';
class GpsHistoryScreen extends StatefulWidget { class GpsHistoryScreen extends StatefulWidget {
const GpsHistoryScreen({Key? key}) : super(key: key); const GpsHistoryScreen({super.key});
@override @override
State<GpsHistoryScreen> createState() => _GpsHistoryScreenState(); State<GpsHistoryScreen> createState() => _GpsHistoryScreenState();

View file

@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; 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:share_plus/share_plus.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:printing/printing.dart'; import 'package:intl/intl.dart';
import 'dart:typed_data'; import 'invoice_input_screen.dart';
import '../widgets/invoice_pdf_preview_page.dart'; import '../widgets/invoice_pdf_preview_page.dart';
import '../models/invoice_models.dart'; import '../models/invoice_models.dart';
import '../services/pdf_generator.dart'; import '../services/pdf_generator.dart';
@ -17,9 +15,10 @@ import '../widgets/keyboard_inset_wrapper.dart';
class InvoiceDetailPage extends StatefulWidget { class InvoiceDetailPage extends StatefulWidget {
final Invoice invoice; final Invoice invoice;
final bool editable;
final bool isUnlocked; 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 @override
State<InvoiceDetailPage> createState() => _InvoiceDetailPageState(); State<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
@ -132,11 +131,13 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
if (newPath != null) { if (newPath != null) {
final finalInvoice = updatedInvoice.copyWith(filePath: newPath); final finalInvoice = updatedInvoice.copyWith(filePath: newPath);
await _invoiceRepo.saveInvoice(finalInvoice); // await _invoiceRepo.saveInvoice(finalInvoice); //
if (!mounted) return;
setState(() { setState(() {
_currentInvoice = finalInvoice; _currentInvoice = finalInvoice;
_currentFilePath = newPath; _currentFilePath = newPath;
}); });
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('データベースとPDFを更新しました')), const SnackBar(content: Text('データベースとPDFを更新しました')),
); );
@ -145,7 +146,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
void _exportCsv() { void _exportCsv() {
final csvData = _currentInvoice.toCsv(); final csvData = _currentInvoice.toCsv();
Share.share(csvData, subject: '請求書データ_CSV'); SharePlus.instance.share(ShareParams(text: csvData, subject: '請求書データ_CSV'));
} }
@override @override
@ -167,7 +168,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
isDraft ? "伝票詳細" : "販売アシスト1号 伝票詳細", isDraft ? "A3:伝票詳細" : "A3:伝票詳細",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -363,7 +364,10 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
], ],
), ),
const SizedBox(height: 8), 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), const SizedBox(height: 8),
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)), Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}", 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)), Text("メール: ${_currentInvoice.contactEmailSnapshot ?? _currentInvoice.customer.email}", style: TextStyle(color: textColor)),
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[ if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
const SizedBox(height: 8), 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> _openPdf() async => await OpenFilex.open(_currentFilePath!);
Future<void> _sharePdf() async { Future<void> _sharePdf() async {
if (_currentFilePath != null) { 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 { Future<void> _previewPdf() async {
await Navigator.push( await Navigator.push(
context, 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)),
],
),
);
}

View file

@ -13,7 +13,7 @@ class InvoiceHistoryItem extends StatelessWidget {
final VoidCallback? onEdit; final VoidCallback? onEdit;
const InvoiceHistoryItem({ const InvoiceHistoryItem({
Key? key, super.key,
required this.invoice, required this.invoice,
required this.isUnlocked, required this.isUnlocked,
required this.amountFormatter, required this.amountFormatter,
@ -21,7 +21,7 @@ class InvoiceHistoryItem extends StatelessWidget {
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.onEdit, this.onEdit,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -14,7 +14,7 @@ class InvoiceHistoryList extends StatelessWidget {
final void Function(Invoice) onEdit; final void Function(Invoice) onEdit;
const InvoiceHistoryList({ const InvoiceHistoryList({
Key? key, super.key,
required this.invoices, required this.invoices,
required this.isUnlocked, required this.isUnlocked,
required this.amountFormatter, required this.amountFormatter,
@ -22,7 +22,7 @@ class InvoiceHistoryList extends StatelessWidget {
required this.onTap, required this.onTap,
required this.onLongPress, required this.onLongPress,
required this.onEdit, required this.onEdit,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../models/invoice_models.dart'; import '../models/invoice_models.dart';
import '../models/customer_model.dart';
import '../services/invoice_repository.dart'; import '../services/invoice_repository.dart';
import '../services/customer_repository.dart'; import '../services/customer_repository.dart';
import '../services/pdf_generator.dart';
import 'invoice_detail_page.dart'; import 'invoice_detail_page.dart';
import 'management_screen.dart'; import 'management_screen.dart';
import 'product_master_screen.dart'; import 'product_master_screen.dart';
@ -15,12 +13,11 @@ import 'company_info_screen.dart';
import '../widgets/slide_to_unlock.dart'; import '../widgets/slide_to_unlock.dart';
import '../main.dart'; // InvoiceFlowScreen import '../main.dart'; // InvoiceFlowScreen
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:printing/printing.dart';
import '../widgets/invoice_pdf_preview_page.dart'; import '../widgets/invoice_pdf_preview_page.dart';
import 'invoice_history/invoice_history_list.dart'; import 'invoice_history/invoice_history_list.dart';
class InvoiceHistoryScreen extends StatefulWidget { class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key); const InvoiceHistoryScreen({super.key});
@override @override
State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState(); State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState();
@ -98,7 +95,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => InvoiceDetailPage( builder: (context) => InvoiceDetailPage(
invoice: invoice, invoice: invoice,
isUnlocked: _isUnlocked, //
), ),
), ),
); );
@ -272,7 +268,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
MaterialPageRoute(builder: (context) => const CompanyInfoScreen()), MaterialPageRoute(builder: (context) => const CompanyInfoScreen()),
).then((_) => _loadData()); ).then((_) => _loadData());
}, },
child: Text("伝票マスター v$_appVersion"), child: Text("A2:履歴リスト v$_appVersion"),
), ),
backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800, backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800,
actions: [ actions: [
@ -353,7 +349,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => InvoiceDetailPage( builder: (context) => InvoiceDetailPage(
invoice: invoice, invoice: invoice,
isUnlocked: _isUnlocked,
), ),
), ),
); );

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../models/customer_model.dart'; import '../models/customer_model.dart';
import '../models/invoice_models.dart'; import '../models/invoice_models.dart';
@ -9,11 +8,8 @@ import '../services/customer_repository.dart';
import '../widgets/invoice_pdf_preview_page.dart'; import '../widgets/invoice_pdf_preview_page.dart';
import 'invoice_detail_page.dart'; import 'invoice_detail_page.dart';
import '../services/gps_service.dart'; import '../services/gps_service.dart';
import 'customer_picker_modal.dart';
import 'customer_master_screen.dart'; import 'customer_master_screen.dart';
import 'product_picker_modal.dart'; import 'product_picker_modal.dart';
import '../models/company_model.dart';
import '../services/company_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart'; import '../widgets/keyboard_inset_wrapper.dart';
class InvoiceInputForm extends StatefulWidget { class InvoiceInputForm extends StatefulWidget {
@ -21,10 +17,10 @@ class InvoiceInputForm extends StatefulWidget {
final Invoice? existingInvoice; // : final Invoice? existingInvoice; // :
const InvoiceInputForm({ const InvoiceInputForm({
Key? key, super.key,
required this.onInvoiceGenerated, required this.onInvoiceGenerated,
this.existingInvoice, // this.existingInvoice, //
}) : super(key: key); });
@override @override
State<InvoiceInputForm> createState() => _InvoiceInputFormState(); State<InvoiceInputForm> createState() => _InvoiceInputFormState();
@ -37,16 +33,14 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
final List<InvoiceItem> _items = []; final List<InvoiceItem> _items = [];
double _taxRate = 0.10; double _taxRate = 0.10;
bool _includeTax = false; bool _includeTax = false;
CompanyInfo? _companyInfo;
DocumentType _documentType = DocumentType.invoice; // DocumentType _documentType = DocumentType.invoice; //
DateTime _selectedDate = DateTime.now(); // : DateTime _selectedDate = DateTime.now(); // :
bool _isDraft = true; // bool _isDraft = true; //
final TextEditingController _subjectController = TextEditingController(); // final TextEditingController _subjectController = TextEditingController(); //
bool _isSaving = false; // bool _isSaving = false; //
String _status = "取引先と商品を入力してください";
// //
List<Offset?> _signaturePath = []; final List<Offset?> _signaturePath = [];
@override @override
void initState() { void initState() {
@ -62,10 +56,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
setState(() => _selectedCustomer = customers.first); setState(() => _selectedCustomer = customers.first);
} }
final companyRepo = CompanyRepository();
final companyInfo = await companyRepo.getCompanyInfo();
setState(() { setState(() {
_companyInfo = companyInfo;
// //
if (widget.existingInvoice != null) { if (widget.existingInvoice != null) {
final inv = widget.existingInvoice!; 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 _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 { Future<void> _saveInvoice({bool generatePdf = true}) async {
if (_selectedCustomer == null) { if (_selectedCustomer == null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("取引先を選択してください"))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("取引先を選択してください")));
@ -138,7 +126,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
try { try {
// PDF生成有無に関わらず // PDF生成有無に関わらず
if (generatePdf) { if (generatePdf) {
setState(() => _status = "PDFを生成中...");
final path = await generateInvoicePdf(invoice); final path = await generateInvoicePdf(invoice);
if (path != null) { if (path != null) {
final updatedInvoice = invoice.copyWith(filePath: path); final updatedInvoice = invoice.copyWith(filePath: path);
@ -189,9 +176,10 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
final newPath = await generateInvoicePdf(promoted); final newPath = await generateInvoicePdf(promoted);
final saved = newPath != null ? promoted.copyWith(filePath: newPath) : promoted; final saved = newPath != null ? promoted.copyWith(filePath: newPath) : promoted;
await _invoiceRepo.saveInvoice(saved); await _invoiceRepo.saveInvoice(saved);
if (!mounted) return false; if (!context.mounted) return false;
Navigator.pop(context); // close preview Navigator.pop(context); // close preview
Navigator.pop(context); // exit edit screen Navigator.pop(context); // exit edit screen
if (!context.mounted) return false;
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -223,7 +211,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), leading: const BackButton(),
title: const Text("販売アシスト1号 V1.5.08"), title: const Text("A1:伝票入力"),
), ),
body: Stack( body: Stack(
children: [ children: [
@ -577,7 +565,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
style: TextStyle(color: textColor), style: TextStyle(color: textColor),
decoration: InputDecoration( decoration: InputDecoration(
hintText: "例:事務所改修工事 / 〇〇月分リース料", hintText: "例:事務所改修工事 / 〇〇月分リース料",
hintStyle: TextStyle(color: textColor.withOpacity(0.5)), hintStyle: TextStyle(color: textColor.withAlpha((0.5 * 255).round())),
filled: true, filled: true,
fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100, fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../services/invoice_repository.dart'; import '../services/invoice_repository.dart';
import '../services/customer_repository.dart'; import '../services/customer_repository.dart';
import 'product_master_screen.dart'; import 'product_master_screen.dart';
@ -12,9 +11,14 @@ import 'activity_log_screen.dart';
import 'sales_report_screen.dart'; import 'sales_report_screen.dart';
import 'gps_history_screen.dart'; import 'gps_history_screen.dart';
class ManagementScreen extends StatelessWidget { class ManagementScreen extends StatefulWidget {
const ManagementScreen({Key? key}) : super(key: key); const ManagementScreen({super.key});
@override
State<ManagementScreen> createState() => _ManagementScreenState();
}
class _ManagementScreenState extends State<ManagementScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -144,14 +148,24 @@ class ManagementScreen extends StatelessWidget {
buffer.writeln("${inv.date},$inv.invoiceNumber,${inv.customer.formalName},${inv.totalAmount},${inv.notes ?? ""}"); 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 { Future<void> _backupDatabase(BuildContext context) async {
final dbPath = p.join(await getDatabasesPath(), 'gemi_invoice.db'); final dbPath = p.join(await getDatabasesPath(), 'gemi_invoice.db');
final file = File(dbPath); final file = File(dbPath);
if (await file.exists()) { if (await file.exists()) {
await Share.shareXFiles([XFile(dbPath)], text: '販売アシスト1号_DBバックアップ'); await SharePlus.instance.share(
ShareParams(
text: '販売アシスト1号_DBバックアップ',
files: [XFile(dbPath)],
),
);
} else { } else {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("データベースファイルが見つかりません"))); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("データベースファイルが見つかりません")));

View file

@ -6,7 +6,7 @@ import 'barcode_scanner_screen.dart';
import '../widgets/keyboard_inset_wrapper.dart'; import '../widgets/keyboard_inset_wrapper.dart';
class ProductMasterScreen extends StatefulWidget { class ProductMasterScreen extends StatefulWidget {
const ProductMasterScreen({Key? key}) : super(key: key); const ProductMasterScreen({super.key});
@override @override
State<ProductMasterScreen> createState() => _ProductMasterScreenState(); State<ProductMasterScreen> createState() => _ProductMasterScreenState();
@ -30,6 +30,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
Future<void> _loadProducts() async { Future<void> _loadProducts() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
final products = await _productRepo.getAllProducts(); final products = await _productRepo.getAllProducts();
if (!mounted) return;
setState(() { setState(() {
_products = products; _products = products;
_isLoading = false; _isLoading = false;
@ -121,6 +122,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
); );
if (result != null) { if (result != null) {
if (!mounted) return;
await _productRepo.saveProduct(result); await _productRepo.saveProduct(result);
_loadProducts(); _loadProducts();
} }
@ -131,7 +133,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: const BackButton(), leading: const BackButton(),
title: const Text("商品マスター"), title: const Text("P1:商品マスター"),
backgroundColor: Colors.blueGrey, backgroundColor: Colors.blueGrey,
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(60), preferredSize: const Size.fromHeight(60),
@ -192,9 +194,9 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _showEditDialog(), onPressed: () => _showEditDialog(),
child: const Icon(Icons.add),
backgroundColor: Colors.blueGrey.shade800, backgroundColor: Colors.blueGrey.shade800,
foregroundColor: Colors.white, foregroundColor: Colors.white,
child: const Icon(Icons.add),
), ),
); );
} }
@ -229,40 +231,41 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
Row( Row(
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(Icons.edit),
label: const Text("編集"),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
_showEditDialog(product: p); _showEditDialog(product: p);
}, },
icon: const Icon(Icons.edit),
label: const Text("編集"),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
if (!p.isLocked) if (!p.isLocked)
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () { icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
showDialog( label: const Text("削除", style: TextStyle(color: Colors.redAccent)),
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("削除の確認"), title: const Text("削除の確認"),
content: Text("${p.name}を削除してよろしいですか?"), content: Text("${p.name}を削除してよろしいですか?"),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")), TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton( TextButton(
onPressed: () async { onPressed: () => Navigator.pop(context, true),
await _productRepo.deleteProduct(p.id);
if (!mounted) return;
Navigator.pop(context); // dialog
Navigator.pop(context); // sheet
_loadProducts();
},
child: const Text("削除", style: TextStyle(color: Colors.red)), 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) if (p.isLocked)
Padding( Padding(

View file

@ -8,7 +8,7 @@ import 'product_master_screen.dart';
class ProductPickerModal extends StatefulWidget { class ProductPickerModal extends StatefulWidget {
final Function(InvoiceItem) onItemSelected; final Function(InvoiceItem) onItemSelected;
const ProductPickerModal({Key? key, required this.onItemSelected}) : super(key: key); const ProductPickerModal({super.key, required this.onItemSelected});
@override @override
State<ProductPickerModal> createState() => _ProductPickerModalState(); State<ProductPickerModal> createState() => _ProductPickerModalState();

View file

@ -3,7 +3,7 @@ import 'package:intl/intl.dart';
import '../services/invoice_repository.dart'; import '../services/invoice_repository.dart';
class SalesReportScreen extends StatefulWidget { class SalesReportScreen extends StatefulWidget {
const SalesReportScreen({Key? key}) : super(key: key); const SalesReportScreen({super.key});
@override @override
State<SalesReportScreen> createState() => _SalesReportScreenState(); State<SalesReportScreen> createState() => _SalesReportScreenState();

View file

@ -230,7 +230,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
title: const Text('設定'), title: const Text('S1:設定'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.info_outline), icon: const Icon(Icons.info_outline),
@ -362,23 +362,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: 'テーマ選択', title: 'テーマ選択',
subtitle: '配色や見た目を切り替え(テンプレ)', subtitle: '配色や見た目を切り替え(テンプレ)',
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
RadioListTile<String>( DropdownButtonFormField<String>(
value: 'light', initialValue: _theme,
groupValue: _theme, decoration: const InputDecoration(labelText: 'テーマを選択'),
title: const Text('ライト'), items: const [
onChanged: (v) => setState(() => _theme = v ?? 'light'), DropdownMenuItem(value: 'light', child: Text('ライト')),
), DropdownMenuItem(value: 'dark', child: Text('ダーク')),
RadioListTile<String>( DropdownMenuItem(value: 'system', child: Text('システムに従う')),
value: 'dark', ],
groupValue: _theme,
title: const Text('ダーク'),
onChanged: (v) => setState(() => _theme = v ?? 'dark'),
),
RadioListTile<String>(
value: 'system',
groupValue: _theme,
title: const Text('システムに従う'),
onChanged: (v) => setState(() => _theme = v ?? 'system'), onChanged: (v) => setState(() => _theme = v ?? 'system'),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

View file

@ -309,7 +309,7 @@ class DatabaseHelper {
Future<void> _safeAddColumn(Database db, String table, String columnDef) async { Future<void> _safeAddColumn(Database db, String table, String columnDef) async {
try { try {
await db.execute('ALTER TABLE ' + table + ' ADD COLUMN ' + columnDef); await db.execute('ALTER TABLE $table ADD COLUMN $columnDef');
} catch (_) { } catch (_) {
// Ignore if the column already exists. // Ignore if the column already exists.
} }

View file

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../models/invoice_models.dart'; import '../models/invoice_models.dart';
import 'company_repository.dart'; import 'company_repository.dart';
@ -156,7 +155,7 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
), ),
), ),
pw.SizedBox(height: 20), pw.SizedBox(height: 20),
pw.Table.fromTextArray( pw.TableHelper.fromTextArray(
headers: const ["品名", "数量", "単価", "金額"], headers: const ["品名", "数量", "単価", "金額"],
data: invoice.items data: invoice.items
.map((item) => [ .map((item) => [
@ -249,8 +248,6 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
final String hash = invoice.contentHash; final String hash = invoice.contentHash;
final String dateStr = DateFormat('yyyyMMdd').format(invoice.date); final String dateStr = DateFormat('yyyyMMdd').format(invoice.date);
final String amountStr = NumberFormat("#,###").format(invoice.totalAmount); final String amountStr = NumberFormat("#,###").format(invoice.totalAmount);
final String subjectStr = invoice.subject?.isNotEmpty == true ? "_${invoice.subject}" : "";
// {}({}){}_{}_{}_{HASH下8桁}.pdf // {}({}){}_{}_{}_{HASH下8桁}.pdf
// //
String safeCustomerName = invoice.customerNameForDisplay String safeCustomerName = invoice.customerNameForDisplay
@ -265,7 +262,8 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
.replaceAll('(同)', '') .replaceAll('(同)', '')
.trim(); .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(); final directory = await getExternalStorageDirectory();
if (directory == null) return null; 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);
}

View file

@ -20,7 +20,7 @@ class InvoicePdfPreviewPage extends StatelessWidget {
final bool showPrint; final bool showPrint;
const InvoicePdfPreviewPage({ const InvoicePdfPreviewPage({
Key? key, super.key,
required this.invoice, required this.invoice,
this.allowFormalIssue = true, this.allowFormalIssue = true,
this.isUnlocked = false, this.isUnlocked = false,
@ -29,7 +29,7 @@ class InvoicePdfPreviewPage extends StatelessWidget {
this.showShare = true, this.showShare = true,
this.showEmail = true, this.showEmail = true,
this.showPrint = true, this.showPrint = true,
}) : super(key: key); });
Future<Uint8List> _buildPdfBytes() async { Future<Uint8List> _buildPdfBytes() async {
final doc = await buildInvoiceDocument(invoice); final doc = await buildInvoiceDocument(invoice);

View file

@ -10,13 +10,13 @@ class KeyboardInsetWrapper extends StatelessWidget {
final Curve curve; final Curve curve;
const KeyboardInsetWrapper({ const KeyboardInsetWrapper({
Key? key, super.key,
required this.child, required this.child,
this.basePadding = EdgeInsets.zero, this.basePadding = EdgeInsets.zero,
this.extraBottom = 0, this.extraBottom = 0,
this.duration = const Duration(milliseconds: 180), this.duration = const Duration(milliseconds: 180),
this.curve = Curves.easeOut, this.curve = Curves.easeOut,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -6,11 +6,11 @@ class SlideToUnlock extends StatefulWidget {
final bool isLocked; final bool isLocked;
const SlideToUnlock({ const SlideToUnlock({
Key? key, super.key,
required this.onUnlocked, required this.onUnlocked,
this.text = "スライドして解除", this.text = "スライドして解除",
this.isLocked = true, this.isLocked = true,
}) : super(key: key); });
@override @override
State<SlideToUnlock> createState() => _SlideToUnlockState(); State<SlideToUnlock> createState() => _SlideToUnlockState();