179 lines
6.4 KiB
Dart
179 lines
6.4 KiB
Dart
// 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')),
|
||
),
|
||
]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|