// version: 1.2.0 (Share-ready, Bottom-8 Hash, Template Edition) import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; // パッケージ import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; // 共有機能 import 'package:pdf/widgets.dart' as pw; import 'package:path_provider/path_provider.dart'; import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; void main() => runApp(const MaterialApp(home: InvoiceApp())); class InvoiceApp extends StatefulWidget { const InvoiceApp({super.key}); @override State createState() => _InvoiceAppState(); } class _InvoiceAppState extends State { final _clientController = TextEditingController(text: "佐々木製作所"); final _amountController = TextEditingController(text: "250000"); String _status = "内容を入力してPDFを発行してください"; // 【設定】ファイル名のフォーマット定数 static const String filenameTemplate = "{date}(請求){name}_{amount}円_{hash}.pdf"; // --- 電話帳から選ぶ --- Future _pickContact() async { try { var status = await Permission.contacts.request(); if (status.isGranted) { final contact = await FlutterContacts.openExternalPick(); if (contact != null) { final fullContact = await FlutterContacts.getContact(contact.id); setState(() { String name = fullContact?.displayName ?? "名称不明"; _clientController.text = name; _status = "連絡先から「$name」を読み込みました"; }); } } else if (status.isPermanentlyDenied) { openAppSettings(); } } catch (e) { setState(() => _status = "エラー: $e"); } } // --- PDF発行 & 共有ロジック --- Future _generateInvoice() async { final pdf = pw.Document(); // フォント読み込み final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf"); final ttf = pw.Font.ttf(fontData); final myTheme = pw.ThemeData.withFont(base: ttf, bold: ttf); final clientName = _clientController.text; final int unitPrice = int.tryParse(_amountController.text) ?? 0; final int tax = (unitPrice * 0.1).floor(); final int total = unitPrice + tax; final now = DateTime.now(); final dateStr = DateFormat('yyyy/MM/dd HH:mm').format(now); final dateFileStr = DateFormat('yyyyMMdd').format(now); final amountFormatter = NumberFormat("#,###"); // PDFレイアウト作成 pdf.addPage( pw.Page( theme: myTheme, build: (context) => pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text("請求書 (Invoice)", style: pw.TextStyle(fontSize: 30)), pw.Divider(), pw.Text("宛名: $clientName 様"), pw.Text("日付: $dateStr"), pw.SizedBox(height: 20), pw.Text("単価(税抜): ${amountFormatter.format(unitPrice)} 円"), pw.Text("消費税 (10%): ${amountFormatter.format(tax)} 円"), pw.Text("合計金額(税込): ${amountFormatter.format(total)} 円", style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 20)), pw.SizedBox(height: 40), pw.Text("※本証憑はSHA-256ハッシュにより改ざん耐性を担保しています。"), ], ), ), ); final Uint8List bytes = await pdf.save(); // SHA-256ハッシュ計算(末尾8文字) final String fullHash = sha256.convert(bytes).toString(); final String hash = fullHash.substring(fullHash.length - 8); // ファイル名生成 String fileName = filenameTemplate .replaceAll("{date}", dateFileStr) .replaceAll("{name}", clientName) .replaceAll("{amount}", amountFormatter.format(total)) .replaceAll("{hash}", hash); // 一時保存 final directory = await getTemporaryDirectory(); // 共有用なので一時フォルダでOK final file = File("${directory.path}/$fileName"); await file.writeAsBytes(bytes); // 【新機能】共有メニューを起動! await Share.shareXFiles( [XFile(file.path)], text: '請求書を送付します: $fileName', subject: '請求書: $clientName 様' ); setState(() { _status = "【発行&共有】\n$fileName\nHASH: $hash"; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("現場のじぇみエモン請求書 V1.2"), backgroundColor: Colors.blueGrey, ), body: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( child: Column(children: [ Row(children: [ Expanded( child: TextField( controller: _clientController, decoration: const InputDecoration(labelText: "取引先名", border: OutlineInputBorder()) ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.contact_mail, color: Colors.blue, size: 36), onPressed: _pickContact, ), ]), const SizedBox(height: 16), TextField( controller: _amountController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: "単価 (税抜)", border: OutlineInputBorder()) ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _generateInvoice, icon: const Icon(Icons.send), label: const Text("PDF発行 & 共有 (Nextcloudへ)"), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 60), backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, ), ), const SizedBox(height: 24), Container( width: double.infinity, padding: const EdgeInsets.all(12), color: Colors.grey[200], child: SelectableText(_status, style: const TextStyle(fontFamily: 'monospace')), ), ]), ), ), ); } }