分割に成功した模様

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';
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(

View file

@ -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(

View file

@ -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),

View file

@ -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 (contacts.isEmpty) {
if (!mounted) return;
if (contacts.isEmpty) {
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(

View file

@ -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,7 +95,12 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
context: context,
builder: (context) => AlertDialog(
title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"),
content: SingleChildScrollView(
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: [
@ -122,12 +133,14 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
],
),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton(
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
title: Text(
_filtered[index].organizations.isNotEmpty && _filtered[index].organizations.first.company.isNotEmpty
? _filtered[index].organizations.first.company
: _filtered[index].displayName),
: _filtered[index].displayName,
),
onTap: () => Navigator.pop(context, _filtered[index]),
),
),

View file

@ -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();

View file

@ -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)),
],
),
);
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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,
),
),
);

View file

@ -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),

View file

@ -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("データベースファイルが見つかりません")));

View file

@ -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(

View file

@ -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();

View file

@ -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();

View file

@ -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),

View file

@ -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.
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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) {

View file

@ -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();