inv/gemi_invoice/lib/main.dart
2026-01-31 15:08:34 +09:00

179 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<InvoiceApp> createState() => _InvoiceAppState();
}
class _InvoiceAppState extends State<InvoiceApp> {
final _clientController = TextEditingController(text: "佐々木製作所");
final _amountController = TextEditingController(text: "250000");
String _status = "内容を入力してPDFを発行してください";
// 【設定】ファイル名のフォーマット定数
static const String filenameTemplate = "{date}(請求){name}_{amount}円_{hash}.pdf";
// --- 電話帳から選ぶ ---
Future<void> _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<void> _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')),
),
]),
),
),
);
}
}