Compare commits

..

1 commit
main ... ollama

Author SHA1 Message Date
user
4f9c24c1a2 ollama/gpt-oss:20bにスイッチ 2026-02-08 10:55:57 +09:00
20 changed files with 425 additions and 1393 deletions

View file

@ -4,6 +4,9 @@ A new Flutter project.
## Getting Started
!! 日本語が主体のアプリです。アプリの基本言語は日本語です。コメントも日本語と英語併記です。!!
!! 可読性は重視しません。AIの開発効率を最大に上げて下さい。!!
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
@ -14,3 +17,28 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## 販売アシスト1号の機能
販売アシスト1号は、営業マンの販売業務を支援するためのアプリケーションです。主な機能は以下の通りです。
- ファイル名の規則 "{date}({請求等}){顧客名}_{件名}_{金額カンマ付}円_{sha256}" 20251202(請求)佐々木製作所_あの件_20,000円_25ab85cc9988
- 伝票(件名)の管理
- 管理ではファイルの様にオブジェクトとして管理します。
- 顧客マスター管理
- 商品マスター管理
- 見積納品請求領収証の作成
- 見積納品請求領収証の一覧表示
- 見積納品請求領収証のPDF出力
- PDFの管理
- GPSと顧客マスターを連携し座標の履歴を記録する機能
- フォーム入力時にGPS情報で自動的に顧客マスターから候補を選出する機能
- 入力時にQRコードやOCRをGoogleレンズを使ってアシストします
- レシート印刷するポータブルプリンタを使って領収証印刷する機能
- 印刷時にQRコードを印刷する機能
- Googleドライブにバックアップする機能
- 未来の機能(今は実装しない可能にしておく)odooとの同期・連携 伝票単位で同期
アプリの基本言語は日本語です。コメントも日本語と英語併記です。可読性は重視しません。AIの開発効率を最大に上げるものです。
表示言語は日本語です。環境により英語を表示する機能は未来に実装予定です。
見積・納品・請求・領収証はフォームがほぼ同じですが、項目が時間と共に変化するので件名が同じでも明細は異なる場合があります。
伝票はsqliteで管理します。

View file

@ -1,19 +1,17 @@
// lib/main.dart
// version: 1.4.3c (Bug Fix: PDF layout error) - Refactored for modularity and history management
// version: 1.4.3c (Bug Fix: PDF layout error) - Refactored for modularity
import 'package:flutter/material.dart';
import 'screens/pdf_list_screen.dart'; // PDF一覧画面
// --- ---
import 'models/invoice_models.dart';
import 'screens/invoice_input_screen.dart';
import 'screens/invoice_detail_page.dart';
import 'screens/invoice_history_screen.dart';
import 'screens/company_editor_screen.dart'; //
import 'models/invoice_models.dart'; // Invoice, InvoiceItem
import 'screens/invoice_input_screen.dart'; //
import 'screens/invoice_detail_page.dart'; //
void main() {
runApp(const MyApp());
}
//
class MyApp extends StatelessWidget {
const MyApp({super.key});
@ -25,87 +23,30 @@ class MyApp extends StatelessWidget {
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
fontFamily: 'IPAexGothic',
),
home: const MainNavigationShell(),
home: const InvoiceFlowScreen(),
);
}
}
///
class MainNavigationShell extends StatefulWidget {
const MainNavigationShell({super.key});
class InvoiceFlowScreen extends StatefulWidget {
const InvoiceFlowScreen({super.key});
@override
State<MainNavigationShell> createState() => _MainNavigationShellState();
State<InvoiceFlowScreen> createState() => _InvoiceFlowScreenState();
}
class _MainNavigationShellState extends State<MainNavigationShell> {
int _selectedIndex = 0;
//
final List<Widget> _screens = [];
@override
void initState() {
super.initState();
_screens.addAll([
InvoiceFlowScreen(onMoveToHistory: () => _onItemTapped(1)),
const InvoiceHistoryScreen(),
]);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _screens,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.add_box),
label: '新規作成',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: '発行履歴',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.indigo,
onTap: _onItemTapped,
),
);
}
}
///
class InvoiceFlowScreen extends StatelessWidget {
final VoidCallback onMoveToHistory;
const InvoiceFlowScreen({super.key, required this.onMoveToHistory});
class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
//
Invoice? _lastGeneratedInvoice;
// PDF
void _handleInvoiceGenerated(BuildContext context, Invoice generatedInvoice, String filePath) {
// PDF生成DB保存後に詳細ページへ遷移
void _handleInvoiceGenerated(Invoice generatedInvoice, String filePath) {
setState(() {
_lastGeneratedInvoice = generatedInvoice;
});
//
Navigator.push(
context,
MaterialPageRoute(
@ -114,31 +55,30 @@ class InvoiceFlowScreen extends StatelessWidget {
);
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//
title: GestureDetector(
onLongPress: () => _openCompanyEditor(context),
child: const Text("販売アシスト1号 V1.4.3c"),
),
title: const Text("販売アシスト1号 V1.4.3c"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.picture_as_pdf),
tooltip: 'PDF一覧',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PdfListScreen(),
),
);
},
),
],
),
//
body: InvoiceInputForm(
onInvoiceGenerated: (invoice, path) => _handleInvoiceGenerated(context, invoice, path),
onInvoiceGenerated: _handleInvoiceGenerated,
),
);
}

View file

@ -1,106 +0,0 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
///
///
class Company {
final String id; // ID ()
final String formalName; // (: )
final String? representative; //
final String? zipCode; // 便
final String? address; //
final String? tel; //
final String? fax; // FAX番号
final String? email; //
final String? website; //
final String? registrationNumber; // ()
final String? notes; //
const Company({
required this.id,
required this.formalName,
this.representative,
this.zipCode,
this.address,
this.tel,
this.fax,
this.email,
this.website,
this.registrationNumber,
this.notes,
});
///
Company copyWith({
String? id,
String? formalName,
String? representative,
String? zipCode,
String? address,
String? tel,
String? fax,
String? email,
String? website,
String? registrationNumber,
String? notes,
}) {
return Company(
id: id ?? this.id,
formalName: formalName ?? this.formalName,
representative: representative ?? this.representative,
zipCode: zipCode ?? this.zipCode,
address: address ?? this.address,
tel: tel ?? this.tel,
fax: fax ?? this.fax,
email: email ?? this.email,
website: website ?? this.website,
registrationNumber: registrationNumber ?? this.registrationNumber,
notes: notes ?? this.notes,
);
}
/// JSON変換 ()
Map<String, dynamic> toJson() {
return {
'id': id,
'formal_name': formalName,
'representative': representative,
'zip_code': zipCode,
'address': address,
'tel': tel,
'fax': fax,
'email': email,
'website': website,
'registration_number': registrationNumber,
'notes': notes,
};
}
/// JSONからモデルを生成
factory Company.fromJson(Map<String, dynamic> json) {
return Company(
id: json['id'] as String,
formalName: json['formal_name'] as String,
representative: json['representative'] as String?,
zipCode: json['zip_code'] as String?,
address: json['address'] as String?,
tel: json['tel'] as String?,
fax: json['fax'] as String?,
email: json['email'] as String?,
website: json['website'] as String?,
registrationNumber: json['registration_number'] as String?,
notes: json['notes'] as String?,
);
}
// ()
static const Company defaultCompany = Company(
id: 'my_company',
formalName: '自社名が入ります',
zipCode: '〒000-0000',
address: '住所がここに入ります',
tel: 'TEL: 00-0000-0000',
registrationNumber: '適格請求書発行事業者登録番号 T1234567890123', //
notes: 'いつもお世話になっております。',
);
}

View file

@ -1,47 +1,31 @@
// lib/models/invoice_models.dart
import 'package:intl/intl.dart';
import 'customer_model.dart';
///
enum DocumentType {
estimate('見積書'),
delivery('納品書'),
invoice('請求書'),
receipt('領収書');
final String label;
const DocumentType(this.label);
}
///
class InvoiceItem {
String description;
int quantity;
int unitPrice;
bool isDiscount; //
InvoiceItem({
required this.description,
required this.quantity,
required this.unitPrice,
this.isDiscount = false, // false ()
});
// ( * )
int get subtotal => quantity * unitPrice * (isDiscount ? -1 : 1);
int get subtotal => quantity * unitPrice;
//
InvoiceItem copyWith({
String? description,
int? quantity,
int? unitPrice,
bool? isDiscount,
}) {
return InvoiceItem(
description: description ?? this.description,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
isDiscount: isDiscount ?? this.isDiscount,
);
}
@ -51,7 +35,6 @@ class InvoiceItem {
'description': description,
'quantity': quantity,
'unit_price': unitPrice,
'is_discount': isDiscount,
};
}
@ -61,12 +44,11 @@ class InvoiceItem {
description: json['description'] as String,
quantity: json['quantity'] as int,
unitPrice: json['unit_price'] as int,
isDiscount: json['is_discount'] ?? false,
);
}
}
/// ()
///
class Invoice {
Customer customer; //
DateTime date;
@ -74,8 +56,6 @@ class Invoice {
String? filePath; // PDFのパス
String invoiceNumber; //
String? notes; //
bool isShared; //
DocumentType type; //
Invoice({
required this.customer,
@ -84,8 +64,6 @@ class Invoice {
this.filePath,
String? invoiceNumber,
this.notes,
this.isShared = false,
this.type = DocumentType.invoice,
}) : invoiceNumber = invoiceNumber ?? DateFormat('yyyyMMdd-HHmm').format(date);
//
@ -114,8 +92,6 @@ class Invoice {
String? filePath,
String? invoiceNumber,
String? notes,
bool? isShared,
DocumentType? type,
}) {
return Invoice(
customer: customer ?? this.customer,
@ -124,23 +100,19 @@ class Invoice {
filePath: filePath ?? this.filePath,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
notes: notes ?? this.notes,
isShared: isShared ?? this.isShared,
type: type ?? this.type,
);
}
// CSV形式への変換
String toCsv() {
StringBuffer sb = StringBuffer();
sb.writeln("Type,${type.label}");
sb.writeln("Customer,${customer.formalName}");
sb.writeln("Number,$invoiceNumber");
sb.writeln("Invoice Number,$invoiceNumber");
sb.writeln("Date,${DateFormat('yyyy/MM/dd').format(date)}");
sb.writeln("Shared,${isShared ? 'Yes' : 'No'}");
sb.writeln("");
sb.writeln("Description,Quantity,UnitPrice,Subtotal,IsDiscount"); // isDiscountを追加
sb.writeln("Description,Quantity,UnitPrice,Subtotal");
for (var item in items) {
sb.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal},${item.isDiscount ? 'Yes' : 'No'}");
sb.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal}");
}
return sb.toString();
}
@ -154,8 +126,6 @@ class Invoice {
'file_path': filePath,
'invoice_number': invoiceNumber,
'notes': notes,
'is_shared': isShared,
'type': type.name, // Enumの名前で保存
};
}
@ -169,12 +139,7 @@ class Invoice {
.toList(),
filePath: json['file_path'] as String?,
invoiceNumber: json['invoice_number'] as String,
notes: (json['notes'] == 'null') ? null : json['notes'] as String?, // 'null'
isShared: json['is_shared'] ?? false,
type: DocumentType.values.firstWhere(
(e) => e.name == (json['type'] ?? 'invoice'),
orElse: () => DocumentType.invoice,
),
notes: json['notes'] as String?,
);
}
}

View file

@ -1,206 +0,0 @@
// lib/screens/company_editor_screen.dart
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/company_model.dart';
import '../services/master_repository.dart';
///
class CompanyEditorScreen extends StatefulWidget {
const CompanyEditorScreen({super.key});
@override
State<CompanyEditorScreen> createState() => _CompanyEditorScreenState();
}
class _CompanyEditorScreenState extends State<CompanyEditorScreen> {
final _repository = MasterRepository();
final _formKey = GlobalKey<FormState>(); //
late Company _company;
late TextEditingController _formalNameController;
late TextEditingController _representativeController;
late TextEditingController _zipCodeController;
late TextEditingController _addressController;
late TextEditingController _telController;
late TextEditingController _faxController;
late TextEditingController _emailController;
late TextEditingController _websiteController;
late TextEditingController _registrationNumberController;
late TextEditingController _notesController;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCompanyInfo();
}
Future<void> _loadCompanyInfo() async {
setState(() => _isLoading = true);
_company = await _repository.loadCompany();
_formalNameController = TextEditingController(text: _company.formalName);
_representativeController = TextEditingController(text: _company.representative);
_zipCodeController = TextEditingController(text: _company.zipCode);
_addressController = TextEditingController(text: _company.address);
_telController = TextEditingController(text: _company.tel);
_faxController = TextEditingController(text: _company.fax);
_emailController = TextEditingController(text: _company.email);
_websiteController = TextEditingController(text: _company.website);
_registrationNumberController = TextEditingController(text: _company.registrationNumber);
_notesController = TextEditingController(text: _company.notes);
setState(() => _isLoading = false);
}
Future<void> _saveCompanyInfo() async {
if (!_formKey.currentState!.validate()) {
return;
}
final updatedCompany = _company.copyWith(
formalName: _formalNameController.text.trim(),
representative: _representativeController.text.trim(),
zipCode: _zipCodeController.text.trim(),
address: _addressController.text.trim(),
tel: _telController.text.trim(),
fax: _faxController.text.trim(),
email: _emailController.text.trim(),
website: _websiteController.text.trim(),
registrationNumber: _registrationNumberController.text.trim(),
notes: _notesController.text.trim(),
);
await _repository.saveCompany(updatedCompany);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('自社情報を保存しました。')),
);
Navigator.pop(context); //
}
}
@override
void dispose() {
_formalNameController.dispose();
_representativeController.dispose();
_zipCodeController.dispose();
_addressController.dispose();
_telController.dispose();
_faxController.dispose();
_emailController.dispose();
_websiteController.dispose();
_registrationNumberController.dispose();
_notesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("自社情報編集"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveCompanyInfo,
tooltip: "保存",
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _formalNameController,
decoration: const InputDecoration(labelText: "正式名称 (必須)", border: OutlineInputBorder()),
validator: (value) {
if (value == null || value.isEmpty) {
return '正式名称は必須です';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _representativeController,
decoration: const InputDecoration(labelText: "代表者名", border: OutlineInputBorder()),
),
const SizedBox(height: 16),
TextFormField(
controller: _zipCodeController,
decoration: const InputDecoration(labelText: "郵便番号", border: OutlineInputBorder()),
keyboardType: TextInputType.text,
),
const SizedBox(height: 16),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: "住所", border: OutlineInputBorder()),
maxLines: 2,
),
const SizedBox(height: 16),
TextFormField(
controller: _telController,
decoration: const InputDecoration(labelText: "電話番号", border: OutlineInputBorder()),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
TextFormField(
controller: _faxController,
decoration: const InputDecoration(labelText: "FAX番号", border: OutlineInputBorder()),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: "メールアドレス", border: OutlineInputBorder()),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(labelText: "ウェブサイト", border: OutlineInputBorder()),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
TextFormField(
controller: _registrationNumberController,
decoration: const InputDecoration(labelText: "登録番号 (インボイス制度対応)", border: OutlineInputBorder()),
),
const SizedBox(height: 16),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
maxLines: 3,
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _saveCompanyInfo,
icon: const Icon(Icons.save),
label: const Text("自社情報を保存"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
),
);
}
}

View file

@ -4,9 +4,8 @@ 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/master_repository.dart';
import 'customer_picker_modal.dart';
import 'product_picker_modal.dart';
class InvoiceDetailPage extends StatefulWidget {
@ -25,9 +24,6 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
late bool _isEditing;
late Invoice _currentInvoice;
String? _currentFilePath;
final _repository = InvoiceRepository();
final ScrollController _scrollController = ScrollController();
bool _userScrolled = false; //
@override
void initState() {
@ -44,7 +40,6 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
void dispose() {
_formalNameController.dispose();
_notesController.dispose();
_scrollController.dispose();
super.dispose();
}
@ -52,16 +47,6 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
setState(() {
_items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0));
});
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_userScrolled && _scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _removeItem(int index) {
@ -70,6 +55,25 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
});
}
void _pickFromMaster() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => FractionallySizedBox(
heightFactor: 0.9,
child: ProductPickerModal(
onItemSelected: (item) {
setState(() {
_items.add(item);
});
Navigator.pop(context);
},
),
),
);
}
Future<void> _saveChanges() async {
final String formalName = _formalNameController.text.trim();
if (formalName.isEmpty) {
@ -87,64 +91,36 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
final updatedInvoice = _currentInvoice.copyWith(
customer: updatedCustomer,
items: _items,
notes: _notesController.text.trim(),
isShared: false, //
notes: _notesController.text,
);
setState(() => _isEditing = false);
// PDFを再生成
final newPath = await generateInvoicePdf(updatedInvoice);
if (newPath != null) {
final finalInvoice = updatedInvoice.copyWith(filePath: newPath);
// DBを更新PDFの物理削除も行われます
await _repository.saveInvoice(finalInvoice);
setState(() {
_currentInvoice = finalInvoice;
_currentInvoice = updatedInvoice.copyWith(filePath: newPath);
_currentFilePath = newPath;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('変更を保存し、PDFを更新しました。')),
);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFの更新に失敗しました')),
);
}
_cancelChanges(); //
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('A4請求書PDFを更新しました')),
);
}
}
void _cancelChanges() {
setState(() {
_isEditing = false;
_formalNameController.text = _currentInvoice.customer.formalName;
_notesController.text = _currentInvoice.notes ?? "";
// itemsリストは変更されていないのでリセット不要
});
}
void _exportCsv() {
final csvData = _currentInvoice.toCsv();
Share.share(csvData, subject: '${_currentInvoice.type.label}データ_CSV');
Share.share(csvData, subject: '請求書データ_CSV');
}
@override
Widget build(BuildContext context) {
final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("¥#,###");
final amountFormatter = NumberFormat("#,###");
return Scaffold(
appBar: AppBar(
title: Text("販売アシスト1号 ${_currentInvoice.type.label}詳細"),
title: const Text("販売アシスト1号 請求書詳細"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
if (!_isEditing) ...[
IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
@ -155,110 +131,78 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
]
],
),
body: NotificationListener<ScrollStartNotification>(
onNotification: (notification) {
//
_userScrolled = true;
return false;
},
child: SingleChildScrollView(
controller: _scrollController, // ScrollController
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(),
const Divider(height: 32),
const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildItemTable(amountFormatter),
if (_isEditing)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Wrap(
spacing: 12,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text("空の行を追加"),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(),
const Divider(height: 32),
const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildItemTable(amountFormatter),
if (_isEditing)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Wrap(
spacing: 12,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text("空の行を追加"),
),
ElevatedButton.icon(
onPressed: _pickFromMaster,
icon: const Icon(Icons.list_alt),
label: const Text("マスターから選択"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey.shade700,
foregroundColor: Colors.white,
),
ElevatedButton.icon(
onPressed: _pickFromMaster,
icon: const Icon(Icons.list_alt),
label: const Text("マスターから選択"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey.shade700,
foregroundColor: Colors.white,
),
),
],
),
),
],
),
const SizedBox(height: 24),
_buildSummarySection(amountFormatter),
const SizedBox(height: 24),
_buildFooterActions(),
],
),
),
const SizedBox(height: 24),
_buildSummarySection(amountFormatter),
const SizedBox(height: 24),
_buildFooterActions(),
],
),
),
);
}
Widget _buildHeaderSection() {
final dateFormatter = DateFormat('yyyy年MM月dd日');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isEditing) ...[
TextFormField(
TextField(
controller: _formalNameController,
decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
onChanged: (value) => setState(() {}), //
),
const SizedBox(height: 12),
TextFormField(
TextField(
controller: _notesController,
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
maxLines: 2,
onChanged: (value) => setState(() {}), //
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
),
] else ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text("${_currentInvoice.customer.formalName} ${_currentInvoice.customer.title}",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis), //
),
if (_currentInvoice.isShared)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade50,
border: Border.all(color: Colors.green),
borderRadius: BorderRadius.circular(4),
),
child: const Row(
children: [
Icon(Icons.check, color: Colors.green, size: 14),
SizedBox(width: 4),
Text("共有済み", style: TextStyle(color: Colors.green, fontSize: 10, fontWeight: FontWeight.bold)),
],
),
),
],
),
Text("${_currentInvoice.customer.formalName} ${_currentInvoice.customer.title}",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 4),
Text("発行日: ${DateFormat('yyyy年MM月dd日').format(_currentInvoice.date)}"),
// InvoiceDetailPageでは unitPrice totalAmount PDF生成時に計算していたため
// `_isEditing` TextField `widget.invoice.unitPrice`
// `_currentInvoice` `unitPrice` `_amountController` 使
// `_currentInvoice.unitPrice` ReadOnly `_amountController` 使
Text("請求番号: ${_currentInvoice.invoiceNumber}"),
Text("発行日: ${dateFormatter.format(_currentInvoice.date)}"),
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
const SizedBox(height: 8),
Text("備考: ${_currentInvoice.notes}", style: const TextStyle(color: Colors.black87)),
]
],
],
);
@ -268,58 +212,52 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
return Table(
border: TableBorder.all(color: Colors.grey.shade300),
columnWidths: const {
0: FlexColumnWidth(4), //
1: FixedColumnWidth(50), //
2: FixedColumnWidth(80), //
3: FlexColumnWidth(2), // ()
4: FixedColumnWidth(40), //
0: FlexColumnWidth(4),
1: FixedColumnWidth(50),
2: FixedColumnWidth(80),
3: FlexColumnWidth(2),
4: FixedColumnWidth(40),
},
verticalAlignment: TableCellVerticalAlignment.middle,
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(
decoration: BoxDecoration(color: Colors.grey.shade100),
children: const [
_TableCell("品名"),
_TableCell("数量"),
_TableCell("単価"),
_TableCell("金額"),
_TableCell(""), //
_TableCell("品名"), _TableCell("数量"), _TableCell("単価"), _TableCell("金額"), _TableCell(""),
],
),
//
..._items.asMap().entries.map((entry) {
int idx = entry.key;
InvoiceItem item = entry.value;
return TableRow(children: [
if (_isEditing)
if (_isEditing) {
return TableRow(children: [
_EditableCell(
initialValue: item.description,
onChanged: (val) => setState(() => item.description = val),
)
else
_TableCell(item.description),
if (_isEditing)
onChanged: (val) => item.description = val,
),
_EditableCell(
initialValue: item.quantity.toString(),
keyboardType: TextInputType.number,
onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0),
)
else
_TableCell(item.quantity.toString()),
if (_isEditing)
),
_EditableCell(
initialValue: item.unitPrice.toString(),
keyboardType: TextInputType.number,
onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0),
)
else
),
_TableCell(formatter.format(item.subtotal)),
IconButton(icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () => _removeItem(idx)),
]);
} else {
return TableRow(children: [
_TableCell(item.description),
_TableCell(item.quantity.toString()),
_TableCell(formatter.format(item.unitPrice)),
_TableCell(formatter.format(item.subtotal)), //
if (_isEditing)
IconButton(icon: const Icon(Icons.delete_outline, size: 20, color: Colors.redAccent), onPressed: () => _removeItem(idx)),
if (!_isEditing) const SizedBox.shrink(), // SizedBox
]);
}).toList(),
_TableCell(formatter.format(item.subtotal)),
const SizedBox(),
]);
}
}),
],
);
}
@ -327,27 +265,22 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
Widget _buildSummarySection(NumberFormat formatter) {
return Align(
alignment: Alignment.centerRight,
child: SizedBox(
child: Container(
width: 200,
child: Column(
children: [
_SummaryRow("小計 (税抜)", formatter.format(_isEditing ? _calculateCurrentSubtotal() : _currentInvoice.subtotal)),
_SummaryRow("消費税 (10%)", formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 0.1).floor() : _currentInvoice.tax)),
const Divider(),
_SummaryRow("合計 (税込)", formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 1.1).floor() : _currentInvoice.totalAmount), isBold: true),
_SummaryRow("合計 (税込)", "${formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 1.1).floor() : _currentInvoice.totalAmount)}", isBold: true),
],
),
),
);
}
//
int _calculateCurrentSubtotal() {
return _items.fold(0, (sum, item) {
//
int price = item.isDiscount ? -item.unitPrice : item.unitPrice;
return sum + (item.quantity * price);
});
return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice));
}
Widget _buildFooterActions() {
@ -367,7 +300,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
child: ElevatedButton.icon(
onPressed: _sharePdf,
icon: const Icon(Icons.share),
label: const Text("共有・送信"),
label: const Text("共有"),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white),
),
),
@ -375,31 +308,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
);
}
Future<void> _openPdf() async {
if (_currentFilePath != null) {
await OpenFilex.open(_currentFilePath!);
}
}
Future<void> _sharePdf() async {
if (_currentFilePath != null) {
await Share.shareXFiles([XFile(_currentFilePath!)], text: '${_currentInvoice.type.label}送付');
// DBに保存
if (!_currentInvoice.isShared) {
final updatedInvoice = _currentInvoice.copyWith(isShared: true);
await _repository.saveInvoice(updatedInvoice);
setState(() {
_currentInvoice = updatedInvoice;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_currentInvoice.type.label}を共有済みとしてマークしました。')),
);
}
}
}
}
Future<void> _openPdf() async => await OpenFilex.open(_currentFilePath!);
Future<void> _sharePdf() async => await Share.shareXFiles([XFile(_currentFilePath!)], text: '請求書送付');
}
class _TableCell extends StatelessWidget {
@ -417,6 +327,7 @@ class _EditableCell extends StatelessWidget {
final TextInputType keyboardType;
final Function(String) onChanged;
const _EditableCell({required this.initialValue, this.keyboardType = TextInputType.text, required this.onChanged});
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
@ -426,8 +337,6 @@ class _EditableCell extends StatelessWidget {
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
onChanged: onChanged,
//
scrollPadding: const EdgeInsets.only(bottom: 100), //
),
);
}
@ -448,37 +357,3 @@ class _SummaryRow extends StatelessWidget {
),
);
}
```
###
1. ****:
* 調 ****
* ****
*
2. ****:
* ****
*
3. **PDF生成への反映**:
* `pdf_generator.dart` PDF生成ロジックで調
4. **UIの微調整**:
* ¥
* `TextFormField` 使Flutterの標準的な挙動では少し難しいのですが
*
### 使
* ****:
1.
* 調 ****
3.
* ****:
1.
* OK

View file

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/invoice_models.dart';
import '../services/invoice_repository.dart';
import 'invoice_detail_page.dart';
///
class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key);
@override
State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState();
}
class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
final InvoiceRepository _repository = InvoiceRepository();
List<Invoice> _invoices = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadInvoices();
}
/// DBから履歴を読み込む
Future<void> _loadInvoices() async {
setState(() => _isLoading = true);
final data = await _repository.getAllInvoices();
setState(() {
_invoices = data;
_isLoading = false;
});
}
/// DBに紐付かないPDFファイルを一括削除
Future<void> _cleanupFiles() async {
final count = await _repository.cleanupOrphanedPdfs();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$count 個の不要なPDFファイルを削除しました')),
);
}
}
///
Future<void> _deleteInvoice(Invoice invoice) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除の確認"),
content: Text("${invoice.type.label}番号: ${invoice.invoiceNumber}\nこのデータを削除しますか?\n(実体PDFファイルも削除されます)"),
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 (confirmed == true) {
await _repository.deleteInvoice(invoice);
_loadInvoices();
}
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
final dateFormatter = DateFormat('yyyy/MM/dd HH:mm');
return Scaffold(
appBar: AppBar(
title: const Text("発行履歴管理"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.cleaning_services),
tooltip: "ゴミファイルを掃除",
onPressed: _cleanupFiles,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadInvoices,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _invoices.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 64, color: Colors.grey.shade300),
const SizedBox(height: 16),
const Text("発行済みの帳票はありません", style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
itemCount: _invoices.length,
itemBuilder: (context, index) {
final invoice = _invoices[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: Stack(
alignment: Alignment.bottomRight,
children: [
const CircleAvatar(
backgroundColor: Colors.indigo,
child: Icon(Icons.description, color: Colors.white),
),
if (invoice.isShared)
Container(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_circle,
color: Colors.green,
size: 18,
),
),
],
),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blueGrey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
invoice.type.label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
invoice.customer.formalName,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("No: ${invoice.invoiceNumber}"),
Text(dateFormatter.format(invoice.date), style: const TextStyle(fontSize: 12)),
],
),
trailing: Text(
"¥${amountFormatter.format(invoice.totalAmount)}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo,
fontSize: 16,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: invoice),
),
);
_loadInvoices();
},
onLongPress: () => _deleteInvoice(invoice),
),
);
},
),
);
}
}

View file

@ -1,14 +1,12 @@
// lib/screens/invoice_input_screen.dart
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/customer_model.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
import '../services/invoice_repository.dart';
import '../services/master_repository.dart';
import 'customer_picker_modal.dart';
///
///
class InvoiceInputForm extends StatefulWidget {
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
@ -24,37 +22,25 @@ class InvoiceInputForm extends StatefulWidget {
class _InvoiceInputFormState extends State<InvoiceInputForm> {
final _clientController = TextEditingController();
final _amountController = TextEditingController(text: "250000");
final _invoiceRepository = InvoiceRepository();
final _masterRepository = MasterRepository();
DocumentType _selectedType = DocumentType.invoice; //
final _repository = InvoiceRepository();
String _status = "取引先を選択してPDFを生成してください";
List<Customer> _customerBuffer = [];
Customer? _selectedCustomer;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadInitialData();
}
_selectedCustomer = Customer(
id: const Uuid().v4(),
displayName: "佐々木製作所",
formalName: "株式会社 佐々木製作所",
);
_customerBuffer.add(_selectedCustomer!);
_clientController.text = _selectedCustomer!.formalName;
///
Future<void> _loadInitialData() async {
setState(() => _isLoading = true);
final savedCustomers = await _masterRepository.loadCustomers();
setState(() {
_customerBuffer = savedCustomers;
if (_customerBuffer.isNotEmpty) {
_selectedCustomer = _customerBuffer.first;
_clientController.text = _selectedCustomer!.formalName;
}
_isLoading = false;
});
_invoiceRepository.cleanupOrphanedPdfs().then((count) {
// PDFを掃除する
_repository.cleanupOrphanedPdfs().then((count) {
if (count > 0) {
debugPrint('Cleaned up $count orphaned PDF files.');
}
@ -68,7 +54,6 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
super.dispose();
}
///
Future<void> _openCustomerPicker() async {
setState(() => _status = "顧客マスターを開いています...");
@ -80,12 +65,10 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
heightFactor: 0.9,
child: CustomerPickerModal(
existingCustomers: _customerBuffer,
onCustomerSelected: (customer) async {
onCustomerSelected: (customer) {
setState(() {
int index = _customerBuffer.indexWhere((c) => c.id == customer.id);
if (index != -1) {
_customerBuffer[index] = customer;
} else {
bool exists = _customerBuffer.any((c) => c.id == customer.id);
if (!exists) {
_customerBuffer.add(customer);
}
@ -93,26 +76,13 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
_clientController.text = customer.formalName;
_status = "${customer.formalName}」を選択しました";
});
await _masterRepository.saveCustomers(_customerBuffer);
if (mounted) Navigator.pop(context);
},
onCustomerDeleted: (customer) async {
setState(() {
_customerBuffer.removeWhere((c) => c.id == customer.id);
if (_selectedCustomer?.id == customer.id) {
_selectedCustomer = null;
_clientController.clear();
}
});
await _masterRepository.saveCustomers(_customerBuffer);
Navigator.pop(context);
},
),
),
);
}
/// PDFを生成して詳細画面へ進む
Future<void> _handleInitialGenerate() async {
if (_selectedCustomer == null) {
setState(() => _status = "取引先を選択してください");
@ -123,7 +93,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
final initialItems = [
InvoiceItem(
description: "${_selectedType.label}",
description: "ご請求",
quantity: 1,
unitPrice: unitPrice,
)
@ -133,17 +103,19 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
customer: _selectedCustomer!,
date: DateTime.now(),
items: initialItems,
type: _selectedType,
);
setState(() => _status = "${_selectedType.label}を生成中...");
setState(() => _status = "A4請求書を生成中...");
final path = await generateInvoicePdf(invoice);
if (path != null) {
final updatedInvoice = invoice.copyWith(filePath: path);
await _invoiceRepository.saveInvoice(updatedInvoice);
// DBに保存
await _repository.saveInvoice(updatedInvoice);
widget.onInvoiceGenerated(updatedInvoice, path);
setState(() => _status = "${_selectedType.label}を生成しDBに登録しました。");
setState(() => _status = "PDFを生成しDBに登録しました。");
} else {
setState(() => _status = "PDFの生成に失敗しました");
}
@ -151,104 +123,76 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"帳票の種類を選択",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
const SizedBox(height: 8),
Wrap(
spacing: 8.0,
children: DocumentType.values.map((type) {
return ChoiceChip(
label: Text(type.label),
selected: _selectedType == type,
onSelected: (selected) {
if (selected) {
setState(() => _selectedType = type);
}
},
selectedColor: Colors.indigo.shade100,
);
}).toList(),
),
const SizedBox(height: 24),
const Text(
"宛先と基本金額の設定",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
const SizedBox(height: 12),
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
readOnly: true,
onTap: _openCustomerPicker,
decoration: const InputDecoration(
labelText: "取引先名 (タップして選択)",
hintText: "マスターから選択または電話帳から取り込み",
prefixIcon: Icon(Icons.business),
border: OutlineInputBorder(),
),
child: Column(children: [
const Text(
"ステップ1: 宛先と基本金額の設定",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
const SizedBox(height: 16),
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
readOnly: true,
onTap: _openCustomerPicker,
decoration: const InputDecoration(
labelText: "取引先名 (タップして選択)",
hintText: "電話帳から取り込むか、マスターから選択",
prefixIcon: Icon(Icons.business),
border: OutlineInputBorder(),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.person_add_alt_1, color: Colors.indigo, size: 40),
onPressed: _openCustomerPicker,
tooltip: "顧客を選択・登録",
),
]),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "基本金額 (税抜)",
hintText: "明細の1行目として登録されます",
prefixIcon: Icon(Icons.currency_yen),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _handleInitialGenerate,
icon: const Icon(Icons.description),
label: Text("${_selectedType.label}を作成して詳細編集へ"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 60),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.person_add_alt_1, color: Colors.indigo, size: 40),
onPressed: _openCustomerPicker,
tooltip: "顧客を選択・登録",
),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
_status,
style: const TextStyle(fontSize: 12, color: Colors.black54),
textAlign: TextAlign.center,
),
]),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "基本金額 (税抜)",
hintText: "明細の1行目として登録されます",
prefixIcon: Icon(Icons.currency_yen),
border: OutlineInputBorder(),
),
],
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _handleInitialGenerate,
icon: const Icon(Icons.description),
label: const Text("A4請求書を作成して詳細編集へ"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 60),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
_status,
style: const TextStyle(fontSize: 12, color: Colors.black54),
textAlign: TextAlign.center,
),
),
]),
),
);
}

View file

@ -0,0 +1,99 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:open_filex/open_filex.dart';
class PdfListScreen extends StatefulWidget {
const PdfListScreen({super.key});
@override
State<PdfListScreen> createState() => _PdfListScreenState();
}
class _PdfListScreenState extends State<PdfListScreen> {
List<FileSystemEntity> _pdfFiles = [];
bool _loading = true;
@override
void initState() {
super.initState();
_fetchPdfFiles();
}
Future<void> _fetchPdfFiles() async {
// Request storage permission
final status = await Permission.storage.request();
if (!status.isGranted) {
setState(() {
_loading = false;
});
return;
}
// Prefer to look in the Downloads folder if available
Directory? downloadsDir;
if (Platform.isAndroid) {
downloadsDir = await getExternalStorageDirectory();
// The Downloads folder may be a subdirectory; adjust as needed
if (downloadsDir != null) {
downloadsDir = Directory('${downloadsDir.path}/Download');
}
} else if (Platform.isIOS) {
downloadsDir = await getApplicationDocumentsDirectory();
} else {
downloadsDir = await getApplicationDocumentsDirectory();
}
if (downloadsDir == null || !await downloadsDir.exists()) {
setState(() {
_loading = false;
});
return;
}
final files = downloadsDir
.listSync()
.where((f) => f.path.toLowerCase().endsWith('.pdf'))
.toList();
setState(() {
_pdfFiles = files;
_loading = false;
});
}
void _openFile(FileSystemEntity file) async {
final result = await OpenFilex.open(file.path);
if (result.type != ResultType.success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open ${file.path}')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('PDF ファイル一覧'),
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _pdfFiles.isEmpty
? const Center(child: Text('PDF ファイルが見つかりません。'))
: ListView.builder(
itemCount: _pdfFiles.length,
itemBuilder: (context, index) {
final file = _pdfFiles[index];
final fileName = file.path.split(Platform.pathSeparator).last;
return ListTile(
leading: const Icon(Icons.picture_as_pdf),
title: Text(fileName),
onTap: () => _openFile(file),
);
},
),
);
}
}

View file

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../data/product_master.dart';
import '../models/invoice_models.dart';
import '../services/master_repository.dart';
///
class ProductPickerModal extends StatefulWidget {
@ -18,31 +17,19 @@ class ProductPickerModal extends StatefulWidget {
}
class _ProductPickerModalState extends State<ProductPickerModal> {
final MasterRepository _masterRepository = MasterRepository();
String _searchQuery = "";
List<Product> _masterProducts = [];
List<Product> _filteredProducts = [];
String _selectedCategory = "すべて";
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadProducts();
// ProductMasterの初期データを使用
_masterProducts = List.from(ProductMaster.products);
_filterProducts();
}
///
Future<void> _loadProducts() async {
setState(() => _isLoading = true);
final products = await _masterRepository.loadProducts();
setState(() {
_masterProducts = products;
_isLoading = false;
_filterProducts();
});
}
///
void _filterProducts() {
setState(() {
_filteredProducts = _masterProducts.where((product) {
@ -96,34 +83,34 @@ class _ProductPickerModalState extends State<ProductPickerModal> {
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton(
onPressed: () async {
onPressed: () {
final String name = nameController.text.trim();
final int price = int.tryParse(priceController.text) ?? 0;
if (name.isEmpty) return;
Product updatedProduct;
if (existingProduct != null) {
updatedProduct = existingProduct.copyWith(
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
);
} else {
updatedProduct = Product(
id: idController.text.isEmpty ? const Uuid().v4().substring(0, 8) : idController.text,
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
);
}
//
await _masterRepository.upsertProduct(updatedProduct);
if (mounted) {
Navigator.pop(context);
_loadProducts(); //
}
setState(() {
if (existingProduct != null) {
//
final index = _masterProducts.indexWhere((p) => p.id == existingProduct.id);
if (index != -1) {
_masterProducts[index] = existingProduct.copyWith(
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
);
}
} else {
//
_masterProducts.add(Product(
id: idController.text.isEmpty ? const Uuid().v4().substring(0, 8) : idController.text,
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
));
}
_filterProducts();
});
Navigator.pop(context);
},
child: const Text("保存"),
),
@ -142,15 +129,12 @@ class _ProductPickerModalState extends State<ProductPickerModal> {
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () async {
onPressed: () {
setState(() {
_masterProducts.removeWhere((p) => p.id == product.id);
});
await _masterRepository.saveProducts(_masterProducts);
if (mounted) {
Navigator.pop(context);
_filterProducts();
}
});
Navigator.pop(context);
},
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
@ -161,10 +145,7 @@ class _ProductPickerModalState extends State<ProductPickerModal> {
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Material(child: Center(child: CircularProgressIndicator()));
}
//
final dynamicCategories = ["すべて", ..._masterProducts.map((p) => p.category ?? 'その他').toSet().toList()];
return Material(

View file

@ -38,17 +38,10 @@ class InvoiceRepository {
//
final index = all.indexWhere((i) => i.invoiceNumber == invoice.invoiceNumber);
if (index != -1) {
final oldInvoice = all[index];
final oldPath = oldInvoice.filePath;
//
// PDFの掃除
final oldPath = all[index].filePath;
if (oldPath != null && oldPath != invoice.filePath) {
//
if (!oldInvoice.isShared) {
await _deletePhysicalFile(oldPath);
} else {
print('Skipping deletion of shared file: $oldPath');
}
await _deletePhysicalFile(oldPath);
}
all[index] = invoice;
} else {
@ -87,11 +80,8 @@ class InvoiceRepository {
}
/// DBに登録されていないPDFファイル
/// DBエントリーのパスは
Future<int> cleanupOrphanedPdfs() async {
final List<Invoice> all = await getAllInvoices();
// DBに登録されている全ての有効なパス
final Set<String> registeredPaths = all
.where((i) => i.filePath != null)
.map((i) => i.filePath!)
@ -105,7 +95,7 @@ class InvoiceRepository {
for (var entity in files) {
if (entity is File && entity.path.endsWith('.pdf')) {
// DBのどの請求データ
// DBに登録されていないPDFは削除
if (!registeredPaths.contains(entity.path)) {
await entity.delete();
deletedCount++;

View file

@ -1,150 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import '../models/customer_model.dart';
import '../models/company_model.dart'; // Companyモデルをインポート
import '../data/product_master.dart';
///
class MasterRepository {
static const String _customerFileName = 'customers_master.json';
static const String _productFileName = 'products_master.json';
static const String _companyFileName = 'company_info.json'; //
///
Future<File> _getCustomerFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_customerFileName');
}
///
Future<File> _getProductFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_productFileName');
}
///
Future<File> _getCompanyFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_companyFileName');
}
// --- ---
///
Future<List<Customer>> loadCustomers() async {
try {
final file = await _getCustomerFile();
if (!await file.exists()) return [];
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Customer.fromJson(j)).toList();
} catch (e) {
debugPrint('Customer Master Loading Error: $e');
return [];
}
}
///
Future<void> saveCustomers(List<Customer> customers) async {
try {
final file = await _getCustomerFile();
final String encoded = json.encode(customers.map((c) => c.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Customer Master Saving Error: $e');
}
}
///
Future<void> upsertCustomer(Customer customer) async {
final customers = await loadCustomers();
final index = customers.indexWhere((c) => c.id == customer.id);
if (index != -1) {
customers[index] = customer;
} else {
customers.add(customer);
}
await saveCustomers(customers);
}
// --- ---
///
/// ProductMasterに定義された初期データを返す
Future<List<Product>> loadProducts() async {
try {
final file = await _getProductFile();
if (!await file.exists()) {
// ProductMasterのハードコードされたリストを返す
return List.from(ProductMaster.products);
}
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Product.fromJson(j)).toList();
} catch (e) {
debugPrint('Product Master Loading Error: $e');
return List.from(ProductMaster.products); //
}
}
///
Future<void> saveProducts(List<Product> products) async {
try {
final file = await _getProductFile();
final String encoded = json.encode(products.map((p) => p.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Product Master Saving Error: $e');
}
}
///
Future<void> upsertProduct(Product product) async {
final products = await loadProducts();
final index = products.indexWhere((p) => p.id == product.id);
if (index != -1) {
products[index] = product;
} else {
products.add(product);
}
await saveProducts(products);
}
// --- ---
///
/// Company.defaultCompany
Future<Company> loadCompany() async {
try {
final file = await _getCompanyFile();
if (!await file.exists()) {
return Company.defaultCompany;
}
final String content = await file.readAsString();
final Map<String, dynamic> jsonMap = json.decode(content);
return Company.fromJson(jsonMap);
} catch (e) {
debugPrint('Company Info Loading Error: $e');
return Company.defaultCompany; //
}
}
///
Future<void> saveCompany(Company company) async {
try {
final file = await _getCompanyFile();
final String encoded = json.encode(company.toJson());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Company Info Saving Error: $e');
}
}
}

View file

@ -1,4 +1,3 @@
// lib/services/pdf_generator.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' show debugPrint;
@ -8,13 +7,9 @@ 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 'package:printing/printing.dart';
import '../models/invoice_models.dart';
import '../models/company_model.dart'; // Companyモデルをインポート
import 'master_repository.dart'; // MasterRepositoryをインポート
/// A4サイズのプロフェッショナルな帳票PDFを生成し
/// DocumentTypeに対応
/// A4サイズのプロフェッショナルな請求書PDFを生成し
Future<String?> generateInvoicePdf(Invoice invoice) async {
try {
final pdf = pw.Document();
@ -24,16 +19,8 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
final ttf = pw.Font.ttf(fontData);
final boldTtf = pw.Font.ttf(fontData); // IPAexGはウェイトが1つなので同じものを使用
//
final MasterRepository masterRepository = MasterRepository();
final Company company = await masterRepository.loadCompany();
final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("¥#,###"); //
//
final String docTitle = invoice.type.label;
final String honorific = " 御中"; // (estimateでもinvoiceでも共通化)
final amountFormatter = NumberFormat("#,###");
pdf.addPage(
pw.MultiPage(
@ -47,11 +34,11 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(docTitle, style: pw.TextStyle(fontSize: 28, fontWeight: pw.FontWeight.bold)),
pw.Text("請求書", style: pw.TextStyle(fontSize: 28, fontWeight: pw.FontWeight.bold)),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text("管理番号: ${invoice.invoiceNumber}"),
pw.Text("請求番号: ${invoice.invoiceNumber}"),
pw.Text("発行日: ${dateFormatter.format(invoice.date)}"),
],
),
@ -68,17 +55,15 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text("${invoice.customer.formalName}$honorific",
style: const pw.TextStyle(fontSize: 18)),
if (invoice.customer.department != null && invoice.customer.department!.isNotEmpty)
pw.Padding(
padding: const pw.EdgeInsets.only(top: 4),
child: pw.Text(invoice.customer.department!),
pw.Container(
decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(width: 1)),
),
child: pw.Text(invoice.customer.invoiceName,
style: const pw.TextStyle(fontSize: 18)),
),
pw.SizedBox(height: 10),
pw.Text(invoice.type == DocumentType.estimate
? "下記の通り、御見積申し上げます。"
: "下記の通り、ご請求申し上げます。"),
pw.Text("下記の通り、ご請求申し上げます。"),
],
),
),
@ -86,11 +71,10 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(company.formalName, style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
if (company.zipCode != null && company.zipCode!.isNotEmpty) pw.Text(company.zipCode!),
if (company.address != null && company.address!.isNotEmpty) pw.Text(company.address!),
if (company.tel != null && company.tel!.isNotEmpty) pw.Text(company.tel!),
if (company.registrationNumber != null && company.registrationNumber!.isNotEmpty) pw.Text(company.registrationNumber! ),
pw.Text("自社名が入ります", style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
pw.Text("〒000-0000"),
pw.Text("住所がここに入ります"),
pw.Text("TEL: 00-0000-0000"),
],
),
),
@ -105,8 +89,8 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text("${docTitle}金額合計 (税込)", style: const pw.TextStyle(fontSize: 16)),
pw.Text("${amountFormatter.format(invoice.totalAmount)} -",
pw.Text("ご請求金額合計 (税込)", style: const pw.TextStyle(fontSize: 16)),
pw.Text("${amountFormatter.format(invoice.totalAmount)} -",
style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold)),
],
),
@ -151,7 +135,7 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
_buildSummaryRow("小計 (税抜)", amountFormatter.format(invoice.subtotal)),
_buildSummaryRow("消費税 (10%)", amountFormatter.format(invoice.tax)),
pw.Divider(),
_buildSummaryRow("合計", amountFormatter.format(invoice.totalAmount), isBold: true),
_buildSummaryRow("合計", "${amountFormatter.format(invoice.totalAmount)}", isBold: true),
],
),
),
@ -166,7 +150,8 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
width: double.infinity,
padding: const pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(border: pw.Border.all(color: PdfColors.grey400)),
child: pw.Text(invoice.notes!)),
child: pw.Text(invoice.notes!),
),
],
],
footer: (context) => pw.Container(
@ -180,17 +165,17 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
),
);
//
final Uint8List bytes = await pdf.save();
final String hash = sha256.convert(bytes).toString().substring(0, 8);
final String dateFileStr = DateFormat('yyyyMMdd').format(invoice.date);
String fileName = "${invoice.type.name}_${dateFileStr}_${invoice.customer.formalName}_$hash.pdf";
String fileName = "Invoice_${dateFileStr}_${invoice.customer.formalName}_$hash.pdf";
final directory = await getExternalStorageDirectory();
if (directory == null) return null;
final file = File("${directory.path}/$fileName");
await file.writeAsBytes(bytes);
return file.path;
} catch (e) {
debugPrint("PDF Generation Error: $e");
@ -198,88 +183,6 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
}
}
/// 58mmレシートPDFを生成して印刷ダイアログを表示する
Future<void> printThermalReceipt(Invoice invoice) async {
try {
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final amountFormatter = NumberFormat("¥#,###"); //
//
final MasterRepository masterRepository = MasterRepository();
final Company company = await masterRepository.loadCompany();
final doc = pw.Document();
doc.addPage(
pw.Page(
// 58mm幅のサーマルプリンタ向け設定 (164pt)
pageFormat: const PdfPageFormat(58 * PdfPageFormat.mm, double.infinity, marginAll: 2 * PdfPageFormat.mm),
theme: pw.ThemeData.withFont(base: ttf),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Center(
child: pw.Text(invoice.type.label, style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
),
pw.SizedBox(height: 5),
pw.Text("${invoice.customer.formalName}", style: const pw.TextStyle(fontSize: 10)),
pw.Divider(thickness: 1, borderStyle: pw.BorderStyle.dashed),
pw.SizedBox(height: 5),
pw.Center(
child: pw.Text(amountFormatter.format(invoice.totalAmount),
style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
),
pw.Center(child: pw.Text("(うち消費税 ${amountFormatter.format(invoice.tax)})", style: const pw.TextStyle(fontSize: 8))),
pw.SizedBox(height: 10),
pw.Text("但し、お品代として", style: const pw.TextStyle(fontSize: 9)),
pw.Text("上記正に領収いたしました", style: const pw.TextStyle(fontSize: 9)),
pw.SizedBox(height: 10),
//
pw.Text("--- 明細 ---\n", style: const pw.TextStyle(fontSize: 8)),
...invoice.items.map((item) => pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Expanded(child: pw.Text(item.description, style: const pw.TextStyle(fontSize: 8))),
pw.Text("x${item.quantity} ", style: const pw.TextStyle(fontSize: 8)),
pw.Text(amountFormatter.format(item.subtotal), style: const pw.TextStyle(fontSize: 8)),
],
)),
pw.Divider(thickness: 0.5),
pw.SizedBox(height: 5),
pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(company.formalName, style: const pw.TextStyle(fontSize: 9)),
pw.Text(DateFormat('yyyy/MM/dd HH:mm').format(invoice.date), style: const pw.TextStyle(fontSize: 7)),
pw.Text("No: ${invoice.invoiceNumber}", style: const pw.TextStyle(fontSize: 7)),
],
),
),
pw.SizedBox(height: 10),
pw.Center(child: pw.Text("ありがとうございました", style: const pw.TextStyle(fontSize: 8))),
pw.SizedBox(height: 20), //
],
);
},
),
);
//
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => doc.save(),
name: "${invoice.type.name}_${invoice.invoiceNumber}",
);
} catch (e) {
debugPrint("Thermal Print Error: $e");
}
}
pw.Widget _buildSummaryRow(String label, String value, {bool isBold = false}) {
final style = pw.TextStyle(fontSize: 12, fontWeight: isBold ? pw.FontWeight.bold : null);
return pw.Padding(

View file

@ -6,13 +6,9 @@
#include "generated_plugin_registrant.h"
#include <printing/printing_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View file

@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
printing
url_launcher_linux
)

View file

@ -5,12 +5,10 @@
import FlutterMacOS
import Foundation
import printing
import share_plus
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View file

@ -172,26 +172,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
version: "1.0.1"
image:
dependency: transitive
description:
@ -292,10 +276,10 @@ packages:
dependency: transitive
description:
name: objective_c
sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e"
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.2.5"
version: "9.3.0"
open_filex:
dependency: "direct main"
description:
@ -376,14 +360,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.11.3"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
permission_handler:
dependency: "direct main"
description:
@ -464,14 +440,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
printing:
dependency: "direct main"
description:
name: printing
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
url: "https://pub.dev"
source: hosted
version: "5.14.2"
pub_semver:
dependency: transitive
description:
@ -513,10 +481,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
@ -630,7 +598,7 @@ packages:
source: hosted
version: "3.1.5"
uuid:
dependency: "direct main"
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8

View file

@ -43,8 +43,6 @@ dependencies:
share_plus: ^12.0.1
url_launcher: ^6.3.2
open_filex: ^4.7.0
printing: ^5.13.2
uuid: ^4.5.1
dev_dependencies:
flutter_test:

View file

@ -7,15 +7,12 @@
#include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <printing/printing_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
printing
share_plus
url_launcher_windows
)