// Version: 1.0 - 見積書用 PDF テンプレート import 'package:flutter/material.dart'; import 'package:flutter_pdfgenerator/flutter_pdfgenerator.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import '../models/customer.dart'; import '../models/product.dart'; import '../models/estimate.dart'; /// 見積書用 PDF テンプレート生成クラス class EstimateTemplate { /// 見積書の PDF を生成 static Future generateEstimatePdf(Estimate estimate) async { final doc = pw.Document(); doc.addPage( pw.MultiPage( pageFormat: PdfPageSize.a5, // A5 サイズ build: (pw.Context context) => [ _buildHeader(context, estimate), ...estimate.items.map((item) => _buildItemLine(item, context)), _buildSummary(context, estimate), _buildFooter(context, estimate), ], ), ); return doc; } /// ヘッダーエリア static pw.Widget _buildHeader(pw.Context context, Estimate estimate) { return pw.Container( padding: const EdgeInsets.all(20), decoration: pw.BoxDecoration(color: PdfColors.white), child: pw.Row( children: [ pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('母艦 お局様', style: pw.TextStyle(fontSize: 16, fontWeight: PdfFontWeight.bold)), pw.Text('会社名:〇〇株式会社'), pw.Text('住所:東京都港区_test 1-1-1'), pw.Text('TEL:03-1234-5678 / FAX:03-1234-5679'), pw.Text('📧 mail@example.com'), ], ), ), pw.Container( padding: const EdgeInsets.all(10), decoration: pw.BoxDecoration(color: PdfColors.blueAccent.withOpacity(0.1)), child: pw.Row( children: [ pw.Text('見積書', style: pw.TextStyle(fontSize: 14, fontWeight: PdfFontWeight.bold)), const Spacer(), pw.Text('No. ${estimate.estimateNumber}', style: pw.TextStyle(fontSize: 12)), ], ), ), ], ), ); } /// 商品明細行 static pw.Widget _buildItemLine(EstimateItem item, pw.Context context) { final isEven = context.pageNumber % 2 == 0; return pw.Container( padding: const EdgeInsets.symmetric(vertical: 2), decoration: pw.BoxDecoration( color: isEven ? PdfColors.grey.shade50 : PdfColors.white, border: Border(top: pw.BorderSide(color: PdfColors.grey.shade300)), ), child: pw.Row( children: [ pw.Text('#${context.pageNumber.toString().padLeft(2, '0')}', style: pw.TextStyle(fontSize: 10)), pw SizedBox(width: 5), pw.Expanded( child: pw.Text(item.productName, style: pw.TextStyle(fontSize: 10, overflow: pw.Overflow.ellipsis)), ), pw Container(width: 20), pw Text('¥${item.unitPrice.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 10)), pw Text(item.quantity.toString(), style: pw.TextStyle(fontSize: 10)), pw Text('¥${item.subtotal.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 10)), ], ), ); } /// 合計金額エリア static pw.Widget _buildSummary(pw.Context context, Estimate estimate) { return pw.Container( padding: const EdgeInsets.all(20), decoration: pw.BoxDecoration(color: PdfColors.blue.shade50), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('合計', style: pw.TextStyle(fontSize: 14, fontWeight: PdfFontWeight.bold)), const SizedBox(height: 8), pw.Text('¥${estimate.totalAmount.toStringAsFixed(0)}', style: pw.TextStyle(fontSize: 16, fontWeight: PdfFontWeight.bold, color: PdfColors.orange.shade500)), const Spacer(), pw.Padding( padding: const EdgeInsets.only(top: 20), child: pw.Text('備考:納期・決済条件などの注意事項', style: pw.TextStyle(fontSize: 10, fontStyle: PdfFontStyle.italic)), ), ], ), ); } /// フッターエリア static pw.Widget _buildFooter(pw.Context context, Estimate estimate) { return pw.Container( padding: const EdgeInsets.all(20), decoration: pw.BoxDecoration(color: PdfColors.white), child: pw.Row( children: [ pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('発行日:${estimate.createdAt.toLocal()}'), if (estimate.expiryDate != null) pw.Text('有効期限:${estimate.expiryDate!.toLocal()}'), ], ), ), pw const Spacer(), pw Container( width: 120, height: 80, decoration: pw.BoxDecoration( color: PdfColors.grey.shade200, child: pw.Center(child: pw.Text('QR コードエリア', style: pw.TextStyle(fontSize: 8))), ), ), ], ), ); } }