feat: Implement customer master management with local storage, GPS history recording, and a new invoice history screen.

This commit is contained in:
joe 2026-02-14 19:36:40 +09:00
parent 2459be9cca
commit 191711803b
18 changed files with 1130 additions and 96 deletions

62
analyze_output.txt Normal file
View file

@ -0,0 +1,62 @@
Analyzing gemi_invoice_backup2...
warning • The value of the field '_lastGeneratedInvoice' isn't used • lib/main.dart:43:12 • unused_field
error • The body might complete normally, causing 'null' to be returned, but the return type, 'Widget', is a potentially non-nullable type • lib/main.dart:61:10 • body_might_complete_normally
warning • The label 'appBar' isn't used • lib/main.dart:62:7 • unused_label
error • Expected to find ';' • lib/main.dart:65:7 • expected_token
error • Expected an identifier • lib/main.dart:65:8 • missing_identifier
error • Unexpected text ';' • lib/main.dart:65:8 • unexpected_token
warning • The label 'drawer' isn't used • lib/main.dart:66:7 • unused_label
error • Expected to find ';' • lib/main.dart:92:7 • expected_token
error • Expected an identifier • lib/main.dart:92:8 • missing_identifier
error • Unexpected text ';' • lib/main.dart:92:8 • unexpected_token
warning • The label 'body' isn't used • lib/main.dart:94:7 • unused_label
error • Expected to find ';' • lib/main.dart:106:7 • expected_token
error • Expected an identifier • lib/main.dart:106:8 • missing_identifier
error • Expected to find ';' • lib/main.dart:106:8 • expected_token
error • Unexpected text ';' • lib/main.dart:106:8 • unexpected_token
error • Expected an identifier • lib/main.dart:107:5 • missing_identifier
error • Unexpected text ';' • lib/main.dart:107:5 • unexpected_token
info • Unnecessary empty statement • lib/main.dart:107:6 • empty_statements
info • The imported package 'uuid' isn't a dependency of the importing package • lib/models/customer_model.dart:1:8 • depend_on_referenced_packages
warning • Unused import: 'package:uuid/uuid.dart' • lib/models/customer_model.dart:1:8 • unused_import
error • The named parameter 'unitPrice' is required, but there's no corresponding argument • lib/models/invoice_models.dart:30:12 • missing_required_argument
error • The named parameter 'unit_price' isn't defined • lib/models/invoice_models.dart:34:7 • undefined_named_parameter
warning • The value of the local variable 'amountFormatter' isn't used • lib/models/invoice_models.dart:88:11 • unused_local_variable
info • The imported package 'uuid' isn't a dependency of the importing package • lib/screens/customer_picker_modal.dart:3:8 • depend_on_referenced_packages
error • Undefined class 'Customer' • lib/screens/customer_picker_modal.dart:8:18 • undefined_class
info • Parameter 'key' could be a super parameter • lib/screens/customer_picker_modal.dart:10:9 • use_super_parameters
error • The name 'Customer' isn't a type, so it can't be used as a type argument • lib/screens/customer_picker_modal.dart:22:8 • non_type_as_type_argument
error • The name 'Customer' isn't a type, so it can't be used as a type argument • lib/screens/customer_picker_modal.dart:23:8 • non_type_as_type_argument
error • The property 'formalName' can't be unconditionally accessed because the receiver can be 'null' • lib/screens/customer_picker_modal.dart:47:25 • unchecked_use_of_nullable_value
error • The property 'displayName' can't be unconditionally accessed because the receiver can be 'null' • lib/screens/customer_picker_modal.dart:48:22 • unchecked_use_of_nullable_value
info • Don't use 'BuildContext's across async gaps • lib/screens/customer_picker_modal.dart:77:28 • use_build_context_synchronously
error • Undefined class 'Customer' • lib/screens/customer_picker_modal.dart:87:5 • undefined_class
error • The method 'Customer' isn't defined for the type '_CustomerPickerModalState' • lib/screens/customer_picker_modal.dart:141:19 • undefined_method
info • Don't use 'BuildContext's across async gaps • lib/screens/customer_picker_modal.dart:150:29 • use_build_context_synchronously
error • Undefined class 'Customer' • lib/screens/customer_picker_modal.dart:164:23 • undefined_class
info • Don't use 'BuildContext's across async gaps • lib/screens/customer_picker_modal.dart:175:29 • use_build_context_synchronously
warning • Unused import: 'dart:io' • lib/screens/invoice_detail_page.dart:1:8 • unused_import
warning • Unused import: '../models/customer_model.dart' • lib/screens/invoice_detail_page.dart:7:8 • unused_import
info • Parameter 'key' could be a super parameter • lib/screens/invoice_detail_page.dart:16:9 • use_super_parameters
info • Don't use 'BuildContext's across async gaps • lib/screens/invoice_detail_page.dart:120:28 • use_build_context_synchronously
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • lib/screens/invoice_detail_page.dart:128:5 • deprecated_member_use
info • 'share' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • lib/screens/invoice_detail_page.dart:128:11 • deprecated_member_use
info • Use a 'SizedBox' to add whitespace to a layout • lib/screens/invoice_detail_page.dart:283:14 • sized_box_for_whitespace
info • 'Share' is deprecated and shouldn't be used. Use SharePlus instead • lib/screens/invoice_detail_page.dart:327:43 • deprecated_member_use
info • 'shareXFiles' is deprecated and shouldn't be used. Use SharePlus.instance.share() instead • lib/screens/invoice_detail_page.dart:327:49 • deprecated_member_use
warning • Unused import: '../models/customer_model.dart' • lib/screens/invoice_history_screen.dart:4:8 • unused_import
info • Parameter 'key' could be a super parameter • lib/screens/invoice_history_screen.dart:10:9 • use_super_parameters
info • The imported package 'uuid' isn't a dependency of the importing package • lib/screens/invoice_input_screen.dart:2:8 • depend_on_referenced_packages
info • Parameter 'key' could be a super parameter • lib/screens/invoice_input_screen.dart:14:9 • use_super_parameters
info • The private field _customerBuffer could be 'final' • lib/screens/invoice_input_screen.dart:29:18 • prefer_final_fields
warning • The value of the field '_customerBuffer' isn't used • lib/screens/invoice_input_screen.dart:29:18 • unused_field
error • A value of type 'Object?' can't be assigned to a variable of type 'Customer?' • lib/screens/invoice_input_screen.dart:87:35 • invalid_assignment
error • The property 'formalName' can't be unconditionally accessed because the receiver can be 'null' • lib/screens/invoice_input_screen.dart:88:49 • unchecked_use_of_nullable_value
error • The property 'formalName' can't be unconditionally accessed because the receiver can be 'null' • lib/screens/invoice_input_screen.dart:89:38 • unchecked_use_of_nullable_value
info • Parameter 'key' could be a super parameter • lib/screens/product_picker_modal.dart:8:9 • use_super_parameters
info • 'desiredAccuracy' is deprecated and shouldn't be used. use settings parameter with AndroidSettings, AppleSettings, WebSettings, or LocationSettings • lib/services/location_service.dart:28:9 • deprecated_member_use
info • 'timeLimit' is deprecated and shouldn't be used. use settings parameter with AndroidSettings, AppleSettings, WebSettings, or LocationSettings • lib/services/location_service.dart:29:9 • deprecated_member_use
info • The import of 'dart:typed_data' is unnecessary because all of the used elements are also provided by the import of 'package:flutter/services.dart' • lib/services/pdf_generator.dart:2:8 • unnecessary_import
58 issues found. (ran in 23.0s)

View file

@ -12,6 +12,12 @@
@import flutter_contacts;
#endif
#if __has_include(<geolocator_apple/GeolocatorPlugin.h>)
#import <geolocator_apple/GeolocatorPlugin.h>
#else
@import geolocator_apple;
#endif
#if __has_include(<open_filex/OpenFilePlugin.h>)
#import <open_filex/OpenFilePlugin.h>
#else
@ -46,6 +52,7 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FlutterContactsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterContactsPlugin"]];
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
[OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]];
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];

View file

@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
import 'models/invoice_models.dart'; // Invoice, InvoiceItem
import 'screens/invoice_input_screen.dart'; //
import 'screens/invoice_detail_page.dart'; //
import 'screens/invoice_history_screen.dart'; //
import 'services/location_service.dart'; //
import 'services/customer_repository.dart'; //
void main() {
runApp(const MyApp());
@ -23,28 +26,23 @@ class MyApp extends StatelessWidget {
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const InvoiceFlowScreen(),
home: const InvoiceHistoryScreen(),
);
}
}
// InvoiceFlowScreen
class InvoiceFlowScreen extends StatefulWidget {
const InvoiceFlowScreen({super.key});
final VoidCallback? onComplete;
const InvoiceFlowScreen({super.key, this.onComplete});
@override
State<InvoiceFlowScreen> createState() => _InvoiceFlowScreenState();
}
class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
//
Invoice? _lastGeneratedInvoice;
// PDF
void _handleInvoiceGenerated(Invoice generatedInvoice, String filePath) {
setState(() {
_lastGeneratedInvoice = generatedInvoice;
});
//
Navigator.push(
context,
@ -61,9 +59,47 @@ class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
title: const Text("販売アシスト1号 V1.4.3c"),
backgroundColor: Colors.blueGrey,
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blueGrey),
child: Text("メニュー", style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(
leading: const Icon(Icons.add_task),
title: const Text("新規伝票作成"),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.history),
title: const Text("伝票履歴"),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const InvoiceHistoryScreen()),
);
},
),
],
),
),
//
body: InvoiceInputForm(
onInvoiceGenerated: _handleInvoiceGenerated,
onInvoiceGenerated: (invoice, path) async {
// GPSの記録を試みる
final locationService = LocationService();
final position = await locationService.getCurrentLocation();
if (position != null) {
final customerRepo = CustomerRepository();
await customerRepo.addGpsHistory(invoice.customer.id, position.latitude, position.longitude);
debugPrint("GPS recorded for customer ${invoice.customer.id}");
}
_handleInvoiceGenerated(invoice, path);
if (widget.onComplete != null) widget.onComplete!();
},
),
);
}

View file

@ -1,4 +1,3 @@
import 'package:uuid/uuid.dart';
class Customer {
final String id;
@ -7,6 +6,10 @@ class Customer {
final String title; // 殿
final String? department; //
final String? address; //
final String? tel; //
final String? odooId; // Odoo側のID
final bool isSynced; //
final DateTime updatedAt; //
Customer({
required this.id,
@ -15,7 +18,11 @@ class Customer {
this.title = "",
this.department,
this.address,
});
this.tel,
this.odooId,
this.isSynced = false,
DateTime? updatedAt,
}) : updatedAt = updatedAt ?? DateTime.now();
String get invoiceName {
String name = formalName;
@ -25,6 +32,36 @@ class Customer {
return "$name $title";
}
Map<String, dynamic> toMap() {
return {
'id': id,
'display_name': displayName,
'formal_name': formalName,
'title': title,
'department': department,
'address': address,
'tel': tel,
'odoo_id': odooId,
'is_synced': isSynced ? 1 : 0,
'updated_at': updatedAt.toIso8601String(),
};
}
factory Customer.fromMap(Map<String, dynamic> map) {
return Customer(
id: map['id'],
displayName: map['display_name'],
formalName: map['formal_name'],
title: map['title'] ?? "",
department: map['department'],
address: map['address'],
tel: map['tel'],
odooId: map['odoo_id'],
isSynced: map['is_synced'] == 1,
updatedAt: DateTime.parse(map['updated_at']),
);
}
Customer copyWith({
String? id,
String? displayName,
@ -32,6 +69,10 @@ class Customer {
String? title,
String? department,
String? address,
String? tel,
String? odooId,
bool? isSynced,
DateTime? updatedAt,
}) {
return Customer(
id: id ?? this.id,
@ -40,6 +81,10 @@ class Customer {
title: title ?? this.title,
department: department ?? this.department,
address: address ?? this.address,
tel: tel ?? this.tel,
odooId: odooId ?? this.odooId,
isSynced: isSynced ?? this.isSynced,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}

View file

@ -2,17 +2,38 @@ import 'customer_model.dart';
import 'package:intl/intl.dart';
class InvoiceItem {
final String? id;
String description;
int quantity;
int unitPrice;
InvoiceItem({
this.id,
required this.description,
required this.quantity,
required this.unitPrice,
});
int get subtotal => quantity * unitPrice;
Map<String, dynamic> toMap(String invoiceId) {
return {
'id': id ?? DateTime.now().microsecondsSinceEpoch.toString(),
'invoice_id': invoiceId,
'description': description,
'quantity': quantity,
'unit_price': unitPrice,
};
}
factory InvoiceItem.fromMap(Map<String, dynamic> map) {
return InvoiceItem(
id: map['id'],
description: map['description'],
quantity: map['quantity'],
unitPrice: map['unit_price'],
);
}
}
class Invoice {
@ -22,6 +43,9 @@ class Invoice {
final List<InvoiceItem> items;
final String? notes;
final String? filePath;
final String? odooId;
final bool isSynced;
final DateTime updatedAt;
Invoice({
String? id,
@ -30,7 +54,11 @@ class Invoice {
required this.items,
this.notes,
this.filePath,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString();
this.odooId,
this.isSynced = false,
DateTime? updatedAt,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
updatedAt = updatedAt ?? DateTime.now();
String get invoiceNumber => "INV-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length > 4 ? id.length - 4 : 0)}";
@ -38,12 +66,27 @@ class Invoice {
int get tax => (subtotal * 0.1).floor();
int get totalAmount => subtotal + tax;
Map<String, dynamic> toMap() {
return {
'id': id,
'customer_id': customer.id,
'date': date.toIso8601String(),
'notes': notes,
'file_path': filePath,
'total_amount': totalAmount,
'odoo_id': odooId,
'is_synced': isSynced ? 1 : 0,
'updated_at': updatedAt.toIso8601String(),
};
}
// : fromMap Customer
// factory
String toCsv() {
final dateFormatter = DateFormat('yyyy/MM/dd');
final amountFormatter = NumberFormat("###");
StringBuffer buffer = StringBuffer();
// ()
buffer.writeln("日付,請求番号,取引先,合計金額,備考");
buffer.writeln("${dateFormatter.format(date)},$invoiceNumber,${customer.formalName},$totalAmount,${notes ?? ""}");
buffer.writeln("");
@ -63,14 +106,20 @@ class Invoice {
List<InvoiceItem>? items,
String? notes,
String? filePath,
String? odooId,
bool? isSynced,
DateTime? updatedAt,
}) {
return Invoice(
id: id ?? this.id,
customer: customer ?? this.customer,
date: date ?? this.date,
items: items ?? List.from(this.items), //
items: items ?? List.from(this.items),
notes: notes ?? this.notes,
filePath: filePath ?? this.filePath,
odooId: odooId ?? this.odooId,
isSynced: isSynced ?? this.isSynced,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}

View file

@ -2,18 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:uuid/uuid.dart';
import '../models/customer_model.dart';
import '../services/customer_repository.dart';
///
class CustomerPickerModal extends StatefulWidget {
final List<Customer> existingCustomers;
final Function(Customer) onCustomerSelected;
final Function(Customer)? onCustomerDeleted; //
const CustomerPickerModal({
Key? key,
required this.existingCustomers,
required this.onCustomerSelected,
this.onCustomerDeleted,
}) : super(key: key);
@override
@ -21,20 +18,33 @@ 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;
@override
void initState() {
super.initState();
_filteredCustomers = widget.existingCustomers;
_loadCustomers();
}
Future<void> _loadCustomers() async {
setState(() => _isLoading = true);
final customers = await _repository.getAllCustomers();
setState(() {
_allCustomers = customers;
_filteredCustomers = customers;
_isLoading = false;
});
}
void _filterCustomers(String query) {
setState(() {
_searchQuery = query.toLowerCase();
_filteredCustomers = widget.existingCustomers.where((customer) {
_filteredCustomers = _allCustomers.where((customer) {
return customer.formalName.toLowerCase().contains(_searchQuery) ||
customer.displayName.toLowerCase().contains(_searchQuery);
}).toList();
@ -50,7 +60,7 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
if (!mounted) return;
setState(() => _isImportingFromContacts = false);
final Contact? selectedContact = await showModalBottomSheet<Contact>(
final Contact? selectedContact = await showModalBottomSheet<Contact?>(
context: context,
isScrollControlled: true,
builder: (context) => _PhoneContactListSelector(contacts: contacts),
@ -121,11 +131,13 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton(
onPressed: () {
onPressed: () async {
final updatedCustomer = existingCustomer?.copyWith(
formalName: formalNameController.text.trim(),
department: departmentController.text.trim(),
address: addressController.text.trim(),
updatedAt: DateTime.now(),
isSynced: false,
) ??
Customer(
id: const Uuid().v4(),
@ -134,10 +146,15 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
department: departmentController.text.trim(),
address: addressController.text.trim(),
);
Navigator.pop(context);
widget.onCustomerSelected(updatedCustomer);
await _repository.saveCustomer(updatedCustomer);
Navigator.pop(context); //
_loadCustomers(); //
//
// widget.onCustomerSelected(updatedCustomer);
},
child: const Text("保存して確定"),
child: const Text("保存してマスターに登録"),
),
],
),
@ -154,14 +171,10 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () {
onPressed: () async {
await _repository.deleteCustomer(customer.id);
Navigator.pop(context);
if (widget.onCustomerDeleted != null) {
widget.onCustomerDeleted!(customer);
setState(() {
_filterCustomers(_searchQuery); //
});
}
_loadCustomers();
},
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
@ -213,7 +226,9 @@ class _CustomerPickerModalState extends State<CustomerPickerModal> {
),
const Divider(),
Expanded(
child: _filteredCustomers.isEmpty
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredCustomers.isEmpty
? const Center(child: Text("該当する顧客がいません"))
: ListView.builder(
itemCount: _filteredCustomers.length,

View file

@ -1,11 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
import 'package:open_filex/open_filex.dart';
import '../models/invoice_models.dart';
import '../models/customer_model.dart';
import '../services/pdf_generator.dart';
import '../services/invoice_repository.dart';
import '../services/customer_repository.dart';
import 'product_picker_modal.dart';
class InvoiceDetailPage extends StatefulWidget {
@ -24,6 +24,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
late bool _isEditing;
late Invoice _currentInvoice;
String? _currentFilePath;
final _invoiceRepo = InvoiceRepository();
final _customerRepo = CustomerRepository();
@override
void initState() {
@ -94,16 +96,27 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
notes: _notesController.text,
);
//
await _invoiceRepo.saveInvoice(updatedInvoice);
//
if (updatedCustomer.formalName != widget.invoice.customer.formalName) {
await _customerRepo.saveCustomer(updatedCustomer);
}
setState(() => _isEditing = false);
final newPath = await generateInvoicePdf(updatedInvoice);
if (newPath != null) {
final finalInvoice = updatedInvoice.copyWith(filePath: newPath);
await _invoiceRepo.saveInvoice(finalInvoice); //
setState(() {
_currentInvoice = updatedInvoice.copyWith(filePath: newPath);
_currentInvoice = finalInvoice;
_currentFilePath = newPath;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('A4請求書PDFを更新しました')),
const SnackBar(content: Text('データベースとPDFを更新しました')),
);
}
}
@ -309,7 +322,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
}
Future<void> _openPdf() async => await OpenFilex.open(_currentFilePath!);
Future<void> _sharePdf() async => await Share.shareXFiles([XFile(_currentFilePath!)], text: '請求書送付');
Future<void> _sharePdf() async {
if (_currentFilePath != null) {
await Share.shareXFiles([XFile(_currentFilePath!)], text: '請求書送付');
}
}
}
class _TableCell extends StatelessWidget {

View file

@ -0,0 +1,304 @@
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 'invoice_detail_page.dart';
import 'management_screen.dart';
import '../main.dart'; // InvoiceFlowScreen
class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key);
@override
State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState();
}
class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
final InvoiceRepository _invoiceRepo = InvoiceRepository();
final CustomerRepository _customerRepo = CustomerRepository();
List<Invoice> _invoices = [];
List<Invoice> _filteredInvoices = [];
bool _isLoading = true;
bool _isUnlocked = false; //
String _searchQuery = "";
String _sortBy = "date"; // "date", "amount", "customer"
DateTime? _startDate;
DateTime? _endDate;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
final customers = await _customerRepo.getAllCustomers();
final invoices = await _invoiceRepo.getAllInvoices(customers);
setState(() {
_invoices = invoices;
_applyFilterAndSort();
_isLoading = false;
});
}
void _applyFilterAndSort() {
setState(() {
_filteredInvoices = _invoices.where((inv) {
final query = _searchQuery.toLowerCase();
final matchesQuery = inv.customer.formalName.toLowerCase().contains(query) ||
inv.invoiceNumber.toLowerCase().contains(query) ||
(inv.notes?.toLowerCase().contains(query) ?? false);
bool matchesDate = true;
if (_startDate != null && inv.date.isBefore(_startDate!)) matchesDate = false;
if (_endDate != null && inv.date.isAfter(_endDate!.add(const Duration(days: 1)))) matchesDate = false;
return matchesQuery && matchesDate;
}).toList();
if (_sortBy == "date") {
_filteredInvoices.sort((a, b) => b.date.compareTo(a.date));
} else if (_sortBy == "amount") {
_filteredInvoices.sort((a, b) => b.totalAmount.compareTo(a.totalAmount));
} else if (_sortBy == "customer") {
_filteredInvoices.sort((a, b) => a.customer.formalName.compareTo(b.customer.formalName));
}
});
}
void _toggleUnlock() {
setState(() {
_isUnlocked = !_isUnlocked;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_isUnlocked ? "編集プロテクトを解除しました" : "編集プロテクトを有効にしました")),
);
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
final dateFormatter = DateFormat('yyyy/MM/dd');
return Scaffold(
appBar: AppBar(
title: const Text("伝票マスター一覧"),
backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800,
actions: [
IconButton(
icon: Icon(_isUnlocked ? Icons.lock_open : Icons.lock, color: _isUnlocked ? Colors.orangeAccent : Colors.white70),
onPressed: _toggleUnlock,
tooltip: _isUnlocked ? "プロテクトする" : "アンロックする",
),
IconButton(
icon: const Icon(Icons.sort),
onPressed: () {
showMenu<String>(
context: context,
position: const RelativeRect.fromLTRB(100, 80, 0, 0),
items: [
const PopupMenuItem(value: "date", child: Text("日付順")),
const PopupMenuItem(value: "amount", child: Text("金額順")),
const PopupMenuItem(value: "customer", child: Text("顧客名順")),
],
).then((val) {
if (val != null) {
setState(() => _sortBy = val);
_applyFilterAndSort();
}
});
},
tooltip: "ソート切り替え",
),
IconButton(
icon: Icon(Icons.date_range, color: (_startDate != null || _endDate != null) ? Colors.orange : Colors.white),
onPressed: () async {
final picked = await showDateRangePicker(
context: context,
initialDateRange: (_startDate != null && _endDate != null)
? DateTimeRange(start: _startDate!, end: _endDate!)
: null,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() {
_startDate = picked.start;
_endDate = picked.end;
});
_applyFilterAndSort();
} else if (_startDate != null) {
//
setState(() {
_startDate = null;
_endDate = null;
});
_applyFilterAndSort();
}
},
tooltip: "日付範囲で絞り込み",
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadData,
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: TextField(
decoration: InputDecoration(
hintText: "検索 (顧客名、伝票番号...)",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
isDense: true,
),
onChanged: (val) {
_searchQuery = val;
_applyFilterAndSort();
},
),
),
),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blueGrey),
child: Text("販売アシスト1号", style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(
leading: const Icon(Icons.history),
title: const Text("伝票マスター一覧"),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.add_task),
title: const Text("新規伝票作成"),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvoiceFlowScreen(onComplete: _loadData)),
);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: const Text("マスター管理・同期"),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ManagementScreen()),
);
},
),
],
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredInvoices.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.folder_open, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(_searchQuery.isEmpty ? "保存された伝票がありません" : "該当する伝票が見つかりません"),
],
),
)
: ListView.builder(
itemCount: _filteredInvoices.length,
itemBuilder: (context, index) {
final invoice = _filteredInvoices[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200,
child: Icon(Icons.description_outlined, color: _isUnlocked ? Colors.indigo : Colors.grey),
),
title: Text(invoice.customer.formalName),
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("${amountFormatter.format(invoice.totalAmount)}",
style: const TextStyle(fontWeight: FontWeight.bold)),
if (invoice.isSynced)
const Icon(Icons.sync, size: 16, color: Colors.green)
else
const Icon(Icons.sync_disabled, size: 16, color: Colors.orange),
],
),
onTap: () async {
if (!_isUnlocked) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("詳細の閲覧・編集にはアンロックが必要です"), duration: Duration(seconds: 1)),
);
return;
}
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: invoice),
),
);
_loadData(); //
},
onLongPress: () async {
if (!_isUnlocked) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("削除するにはアンロックが必要です")),
);
return;
}
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("伝票の削除"),
content: Text("${invoice.customer.formalName}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("削除", style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true) {
await _invoiceRepo.deleteInvoice(invoice.id);
_loadData();
}
},
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceFlowScreen(onComplete: _loadData),
),
);
},
label: const Text("新規伝票作成"),
icon: const Icon(Icons.add),
backgroundColor: Colors.indigo,
),
);
}
}

View file

@ -4,6 +4,7 @@ import '../models/customer_model.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
import '../services/invoice_repository.dart';
import '../services/customer_repository.dart';
import 'customer_picker_modal.dart';
///
@ -25,26 +26,42 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
final _repository = InvoiceRepository();
String _status = "取引先を選択してPDFを生成してください";
List<Customer> _customerBuffer = [];
Customer? _selectedCustomer;
@override
void initState() {
super.initState();
_selectedCustomer = Customer(
id: const Uuid().v4(),
displayName: "佐々木製作所",
formalName: "株式会社 佐々木製作所",
);
_customerBuffer.add(_selectedCustomer!);
_clientController.text = _selectedCustomer!.formalName;
_loadInitialData();
}
Future<void> _loadInitialData() async {
// PDFを掃除する
_repository.cleanupOrphanedPdfs().then((count) {
if (count > 0) {
debugPrint('Cleaned up $count orphaned PDF files.');
}
});
final customerRepo = CustomerRepository();
final customers = await customerRepo.getAllCustomers();
if (customers.isNotEmpty) {
setState(() {
_selectedCustomer = customers.first;
_clientController.text = _selectedCustomer!.formalName;
});
} else {
//
final defaultCustomer = Customer(
id: const Uuid().v4(),
displayName: "佐々木製作所",
formalName: "株式会社 佐々木製作所",
);
await customerRepo.saveCustomer(defaultCustomer);
setState(() {
_selectedCustomer = defaultCustomer;
_clientController.text = _selectedCustomer!.formalName;
});
}
}
@override
@ -64,14 +81,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
builder: (context) => FractionallySizedBox(
heightFactor: 0.9,
child: CustomerPickerModal(
existingCustomers: _customerBuffer,
onCustomerSelected: (customer) {
setState(() {
bool exists = _customerBuffer.any((c) => c.id == customer.id);
if (!exists) {
_customerBuffer.add(customer);
}
_selectedCustomer = customer;
_clientController.text = customer.formalName;
_status = "${customer.formalName}」を選択しました";

View file

@ -0,0 +1,125 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import '../models/invoice_models.dart';
import '../services/invoice_repository.dart';
import '../services/customer_repository.dart';
class ManagementScreen extends StatelessWidget {
const ManagementScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("マスター管理・同期"),
backgroundColor: Colors.blueGrey,
),
body: ListView(
children: [
_buildSectionHeader("データ入出力"),
_buildMenuTile(
context,
Icons.upload_file,
"伝票マスター・エクスポート",
"全データをCSV形式で出力します",
() => _exportAllInvoicesCsv(context),
),
_buildMenuTile(
context,
Icons.download,
"伝票マスター・インポート",
"外部ファイルからデータを取り込みます",
() => _showComingSoon(context),
),
const Divider(),
_buildSectionHeader("バックアップ & セキュリティ"),
_buildMenuTile(
context,
Icons.backup,
"データベース・バックアップ",
"SQLiteファイルを外部へ保存・シェアします",
() => _backupDatabase(context),
),
_buildMenuTile(
context,
Icons.settings_backup_restore,
"データベース・リストア",
"バックアップから全てのデータを復元します",
() => _showComingSoon(context),
),
const Divider(),
_buildSectionHeader("外部同期 (将来のOdoo連携)"),
_buildMenuTile(
context,
Icons.sync,
"クラウド同期を実行",
"未同期の伝票をクラウドマスターへ送信します",
() => _showComingSoon(context),
),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
);
}
Widget _buildMenuTile(BuildContext context, IconData icon, String title, String subtitle, VoidCallback onTap) {
return ListTile(
leading: Icon(icon, color: Colors.blueGrey),
title: Text(title),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
void _showComingSoon(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("この機能は次期バージョンで実装予定です。同期フラグ等の基盤は準備済みです。")),
);
}
Future<void> _exportAllInvoicesCsv(BuildContext context) async {
final invoiceRepo = InvoiceRepository();
final customerRepo = CustomerRepository();
final customers = await customerRepo.getAllCustomers();
final invoices = await invoiceRepo.getAllInvoices(customers);
if (invoices.isEmpty) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("エクスポートするデータがありません")));
return;
}
StringBuffer buffer = StringBuffer();
buffer.writeln("日付,請求番号,取引先,合計金額,備考");
for (var inv in invoices) {
buffer.writeln("${inv.date},$inv.invoiceNumber,${inv.customer.formalName},${inv.totalAmount},${inv.notes ?? ""}");
}
await Share.share(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バックアップ');
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("データベースファイルが見つかりません")));
}
}
}

View file

@ -0,0 +1,64 @@
import 'package:sqflite/sqflite.dart';
import '../models/customer_model.dart';
import 'database_helper.dart';
class CustomerRepository {
final DatabaseHelper _dbHelper = DatabaseHelper();
Future<List<Customer>> getAllCustomers() async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('customers', orderBy: 'display_name ASC');
return List.generate(maps.length, (i) => Customer.fromMap(maps[i]));
}
Future<void> saveCustomer(Customer customer) async {
final db = await _dbHelper.database;
await db.insert(
'customers',
customer.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> deleteCustomer(String id) async {
final db = await _dbHelper.database;
await db.delete('customers', where: 'id = ?', whereArgs: [id]);
}
// GPS履歴の保存 (10)
Future<void> addGpsHistory(String customerId, double latitude, double longitude) async {
final db = await _dbHelper.database;
final now = DateTime.now().toIso8601String();
await db.transaction((txn) async {
//
await txn.insert('customer_gps_history', {
'customer_id': customerId,
'latitude': latitude,
'longitude': longitude,
'timestamp': now,
});
// 10
await txn.execute('''
DELETE FROM customer_gps_history
WHERE id IN (
SELECT id FROM customer_gps_history
WHERE customer_id = ?
ORDER BY timestamp DESC
LIMIT -1 OFFSET 10
)
''', [customerId]);
});
}
Future<List<Map<String, dynamic>>> getGpsHistory(String customerId) async {
final db = await _dbHelper.database;
return await db.query(
'customer_gps_history',
where: 'customer_id = ?',
whereArgs: [customerId],
orderBy: 'timestamp DESC',
);
}
}

View file

@ -0,0 +1,94 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _database;
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'gemi_invoice.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
//
await db.execute('''
CREATE TABLE customers (
id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
formal_name TEXT NOT NULL,
title TEXT DEFAULT '',
department TEXT,
address TEXT,
tel TEXT,
odoo_id TEXT,
is_synced INTEGER DEFAULT 0,
updated_at TEXT NOT NULL
)
''');
// GPS履歴 (10DB上は保持)
await db.execute('''
CREATE TABLE customer_gps_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
timestamp TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE
)
''');
//
await db.execute('''
CREATE TABLE products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
default_unit_price INTEGER,
odoo_id TEXT
)
''');
//
await db.execute('''
CREATE TABLE invoices (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
date TEXT NOT NULL,
notes TEXT,
file_path TEXT,
total_amount INTEGER,
odoo_id TEXT,
is_synced INTEGER DEFAULT 0,
updated_at TEXT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers (id)
)
''');
//
await db.execute('''
CREATE TABLE invoice_items (
id TEXT PRIMARY KEY,
invoice_id TEXT NOT NULL,
description TEXT NOT NULL,
quantity INTEGER NOT NULL,
unit_price INTEGER NOT NULL,
FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE
)
''');
}
}

View file

@ -1,15 +1,101 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import '../models/invoice_models.dart';
import '../models/customer_model.dart';
import 'database_helper.dart';
class InvoiceRepository {
// : SQLite (sqflite) 使
//
final DatabaseHelper _dbHelper = DatabaseHelper();
Future<void> saveInvoice(Invoice invoice) async {
debugPrint("Saving invoice: ${invoice.invoiceNumber} for ${invoice.customer.formalName}");
// TODO: SQLite
final db = await _dbHelper.database;
await db.transaction((txn) async {
//
await txn.insert(
'invoices',
invoice.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
//
await txn.delete(
'invoice_items',
where: 'invoice_id = ?',
whereArgs: [invoice.id],
);
//
for (var item in invoice.items) {
await txn.insert('invoice_items', item.toMap(invoice.id));
}
});
}
Future<List<Invoice>> getAllInvoices(List<Customer> customers) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> invoiceMaps = await db.query('invoices', orderBy: 'date DESC');
List<Invoice> invoices = [];
for (var iMap in invoiceMaps) {
final customer = customers.firstWhere(
(c) => c.id == iMap['customer_id'],
orElse: () => Customer(id: iMap['customer_id'], displayName: "不明", formalName: "不明"),
);
final List<Map<String, dynamic>> itemMaps = await db.query(
'invoice_items',
where: 'invoice_id = ?',
whereArgs: [iMap['id']],
);
final items = List.generate(itemMaps.length, (i) => InvoiceItem.fromMap(itemMaps[i]));
invoices.add(Invoice(
id: iMap['id'],
customer: customer,
date: DateTime.parse(iMap['date']),
items: items,
notes: iMap['notes'],
filePath: iMap['file_path'],
odooId: iMap['odoo_id'],
isSynced: iMap['is_synced'] == 1,
updatedAt: DateTime.parse(iMap['updated_at']),
));
}
return invoices;
}
Future<void> deleteInvoice(String id) async {
final db = await _dbHelper.database;
await db.transaction((txn) async {
// PDFパスの取得
final List<Map<String, dynamic>> maps = await txn.query(
'invoices',
columns: ['file_path'],
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty && maps.first['file_path'] != null) {
final file = File(maps.first['file_path']);
if (await file.exists()) {
await file.delete();
}
}
await txn.delete(
'invoice_items',
where: 'invoice_id = ?',
whereArgs: [id],
);
await txn.delete(
'invoices',
where: 'id = ?',
whereArgs: [id],
);
});
}
Future<int> cleanupOrphanedPdfs() async {
@ -17,13 +103,20 @@ class InvoiceRepository {
final directory = await getExternalStorageDirectory();
if (directory == null) return 0;
final List<FileSystemEntity> files = directory.listSync();
final files = directory.listSync().whereType<File>().toList();
final db = await _dbHelper.database;
final List<Map<String, dynamic>> results = await db.query('invoices', columns: ['file_path']);
final activePaths = results.map((r) => r['file_path'] as String?).where((p) => p != null).toSet();
int count = 0;
//
// 0
for (var file in files) {
if (file.path.endsWith('.pdf') && !activePaths.contains(file.path)) {
await file.delete();
count++;
}
}
return count;
} catch (e) {
debugPrint("Cleanup error: $e");
return 0;
}
}

View file

@ -0,0 +1,37 @@
import 'package:geolocator/geolocator.dart';
class LocationService {
Future<Position?> getCurrentLocation() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return null;
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return null;
}
}
if (permission == LocationPermission.deniedForever) {
return null;
}
try {
const locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
timeLimit: Duration(seconds: 5),
);
return await Geolocator.getCurrentPosition(
locationSettings: locationSettings,
);
} catch (e) {
return null;
}
}
}

View file

@ -5,11 +5,13 @@
import FlutterMacOS
import Foundation
import geolocator_apple
import share_plus
import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View file

@ -160,6 +160,54 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2
url: "https://pub.dev"
source: hosted
version: "13.0.4"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
url: "https://pub.dev"
source: hosted
version: "4.6.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
url: "https://pub.dev"
source: hosted
version: "2.3.13"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
url: "https://pub.dev"
source: hosted
version: "4.2.6"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
url: "https://pub.dev"
source: hosted
version: "4.1.3"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
glob:
dependency: transitive
description:
@ -646,7 +694,7 @@ packages:
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8

View file

@ -45,6 +45,8 @@ dependencies:
open_filex: ^4.7.0
sqflite: ^2.3.0
path: ^1.8.3
geolocator: ^13.0.1
uuid: ^4.5.1
dev_dependencies:
flutter_test:

View file

@ -1,2 +1,25 @@
サンプルを編集できる状態ですが
顧客マスターを実装します
### サンプルを編集できる状態ですが
- 顧客マスターを実装します
- 伝票マスターを実装します
- 将来odooと同期する時に配慮
- 完全にスタンドアローンで稼働するのを強く意識する
- GoogleDrive等にDBをバックアップする可能性
- 伝票マスター一覧画面を実装する
起動したら伝票マスター一覧がトップになる様にする
- 伝票マスターを編集する画面を実装する
- 伝票マスターを新規作成する画面を実装する
- 伝票マスターを削除する画面を実装する
- 伝票マスターを検索する画面を実装する
- 伝票マスターをソートする画面を実装する
- 伝票マスターをフィルタリングする画面を実装する
- 伝票マスターをインポートする画面を実装する
- 伝票マスターをエクスポートする画面を実装する
- 伝票マスターをバックアップする画面を実装する
- 伝票マスターをリストアする画面を実装する
- 伝票マスターを同期する画面を実装する
- 伝票マスターは保護しなければならないのでアンロックする仕組みを作る
- ロックは鍵のマークを横に大きくスワイプして解除したい
- 商品マスター管理画面の実装
- 伝票入力画面の実装
- 伝票入力はあれもこれも盛り込みたいので実験的に色んなのを実装