
# ==========================================
# FLUTTER CODE BUNDLE FOR AI ANALYSIS
# PROJECT: Flutter to Kivy Migration
# ==========================================



--- FILE: main.dart ---
// lib/main.dart
// version: 1.4.3c (Bug Fix: PDF layout error) - Refactored for modularity and history management
import 'package:flutter/material.dart';

// --- 独自モジュールのインポート ---
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'; // 自社情報エディタをインポート

void main() {
  runApp(const MyApp());
}

// アプリケーションのルートウィジェット
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '販売アシスト1号',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        useMaterial3: true,
        fontFamily: 'IPAexGothic',
      ),
      home: const MainNavigationShell(),
    );
  }
}

/// 下部ナビゲーションを管理するメインシェル
class MainNavigationShell extends StatefulWidget {
  const MainNavigationShell({super.key});

  @override
  State<MainNavigationShell> createState() => _MainNavigationShellState();
}

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});

  // PDF 生成後に呼び出され、詳細ページへ遷移するコールバック
  void _handleInvoiceGenerated(BuildContext context, Invoice generatedInvoice, String filePath) {
    // PDF生成・DB保存後に詳細ページへ遷移
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => InvoiceDetailPage(invoice: generatedInvoice),
      ),
    );
  }

  // 自社情報エディタ画面を開く（タイトル長押し用）
  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"),
        ),
        backgroundColor: Colors.blueGrey,
        foregroundColor: Colors.white,
      ),
      // 入力フォームを表示
      body: InvoiceInputForm(
        onInvoiceGenerated: (invoice, path) => _handleInvoiceGenerated(context, invoice, path),
      ),
    );
  }
}



--- FILE: models/invoice_models.dart ---
// 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);

  // 編集用のコピーメソッド
  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,
    );
  }

  // JSON変換
  Map<String, dynamic> toJson() {
    return {
      'description': description,
      'quantity': quantity,
      'unit_price': unitPrice,
      'is_discount': isDiscount,
    };
  }

  // JSONから復元
  factory InvoiceItem.fromJson(Map<String, dynamic> json) {
    return 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;
  List<InvoiceItem> items;
  String? filePath; // 保存されたPDFのパス
  String invoiceNumber; // 請求書番号
  String? notes; // 備考
  bool isShared; // 外部共有（送信）済みフラグ。送信済みファイルは自動削除から保護する。
  DocumentType type; // 帳票の種類

  Invoice({
    required this.customer,
    required this.date,
    required this.items,
    this.filePath,
    String? invoiceNumber,
    this.notes,
    this.isShared = false,
    this.type = DocumentType.invoice,
  }) : invoiceNumber = invoiceNumber ?? DateFormat('yyyyMMdd-HHmm').format(date);

  // 互換性のためのゲッター
  String get clientName => customer.formalName;

  // 税抜合計金額
  int get subtotal {
    return items.fold(0, (sum, item) => sum + item.subtotal);
  }

  // 消費税 (10%固定として計算、端数切り捨て)
  int get tax {
    return (subtotal * 0.1).floor();
  }

  // 税込合計金額
  int get totalAmount {
    return subtotal + tax;
  }

  // 状態更新のためのコピーメソッド
  Invoice copyWith({
    Customer? customer,
    DateTime? date,
    List<InvoiceItem>? items,
    String? filePath,
    String? invoiceNumber,
    String? notes,
    bool? isShared,
    DocumentType? type,
  }) {
    return Invoice(
      customer: customer ?? this.customer,
      date: date ?? this.date,
      items: items ?? this.items,
      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("Date,${DateFormat('yyyy/MM/dd').format(date)}");
    sb.writeln("Shared,${isShared ? 'Yes' : 'No'}");
    sb.writeln("");
    sb.writeln("Description,Quantity,UnitPrice,Subtotal,IsDiscount"); // isDiscountを追加
    for (var item in items) {
      sb.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal},${item.isDiscount ? 'Yes' : 'No'}");
    }
    return sb.toString();
  }

  // JSON変換 (データベース保存用)
  Map<String, dynamic> toJson() {
    return {
      'customer': customer.toJson(),
      'date': date.toIso8601String(),
      'items': items.map((item) => item.toJson()).toList(),
      'file_path': filePath,
      'invoice_number': invoiceNumber,
      'notes': notes,
      'is_shared': isShared,
      'type': type.name, // Enumの名前で保存
    };
  }

  // JSONから復元 (データベース読み込み用)
  factory Invoice.fromJson(Map<String, dynamic> json) {
    return Invoice(
      customer: Customer.fromJson(json['customer'] as Map<String, dynamic>),
      date: DateTime.parse(json['date'] as String),
      items: (json['items'] as List)
          .map((i) => InvoiceItem.fromJson(i as Map<String, dynamic>))
          .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,
      ),
    );
  }
}



--- FILE: models/customer_model.dart ---
import 'package:intl/intl.dart';

/// 顧客情報を管理するモデル
/// 将来的な Odoo 同期を見据えて、外部ID（odooId）を保持できるように設計
class Customer {
  final String id;             // ローカル管理用のID
  final int? odooId;           // Odoo上の res.partner ID (nullの場合は未同期)
  final String displayName;     // 電話帳からの表示名（検索用バッファ）
  final String formalName;      // 請求書に記載する正式名称（株式会社〜 など）
  final String? zipCode;        // 郵便番号
  final String? address;        // 住所
  final String? department;     // 部署名
  final String? title;          // 敬称 (様、御中など。デフォルトは御中)
  final DateTime lastUpdatedAt; // 最終更新日時

  Customer({
    required this.id,
    this.odooId,
    required this.displayName,
    required this.formalName,
    this.zipCode,
    this.address,
    this.department,
    this.title = '御中',
    DateTime? lastUpdatedAt,
  }) : this.lastUpdatedAt = lastUpdatedAt ?? DateTime.now();

  /// 請求書表示用のフルネームを取得
  String get invoiceName => department != null && department!.isNotEmpty
      ? "$formalName\n$department $title"
      : "$formalName $title";

  /// 状態更新のためのコピーメソッド
  Customer copyWith({
    String? id,
    int? odooId,
    String? displayName,
    String? formalName,
    String? zipCode,
    String? address,
    String? department,
    String? title,
    DateTime? lastUpdatedAt,
  }) {
    return Customer(
      id: id ?? this.id,
      odooId: odooId ?? this.odooId,
      displayName: displayName ?? this.displayName,
      formalName: formalName ?? this.formalName,
      zipCode: zipCode ?? this.zipCode,
      address: address ?? this.address,
      department: department ?? this.department,
      title: title ?? this.title,
      lastUpdatedAt: lastUpdatedAt ?? DateTime.now(),
    );
  }

  /// JSON変換 (ローカル保存・Odoo同期用)
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'odoo_id': odooId,
      'display_name': displayName,
      'formal_name': formalName,
      'zip_code': zipCode,
      'address': address,
      'department': department,
      'title': title,
      'last_updated_at': lastUpdatedAt.toIso8601String(),
    };
  }

  /// JSONからモデルを生成
  factory Customer.fromJson(Map<String, dynamic> json) {
    return Customer(
      id: json['id'],
      odooId: json['odoo_id'],
      displayName: json['display_name'],
      formalName: json['formal_name'],
      zipCode: json['zip_code'],
      address: json['address'],
      department: json['department'],
      title: json['title'] ?? '御中',
      lastUpdatedAt: DateTime.parse(json['last_updated_at']),
    );
  }
}



--- FILE: models/company_model.dart ---
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: 'いつもお世話になっております。',
  );
}



--- FILE: services/pdf_generator.dart ---
// lib/services/pdf_generator.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' show debugPrint;
import 'package:flutter/services.dart';
import 'package:pdf/pdf.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';
import 'package:printing/printing.dart';
import '../models/invoice_models.dart';
import '../models/company_model.dart'; // Companyモデルをインポート
import 'master_repository.dart'; // MasterRepositoryをインポート

/// A4サイズのプロフェッショナルな帳票PDFを生成し、保存する
/// 見積書、納品書、請求書、領収書の各DocumentTypeに対応
Future<String?> generateInvoicePdf(Invoice invoice) async {
  try {
    final pdf = pw.Document();

    // フォントのロード
    final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
    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でも共通化)

    pdf.addPage(
      pw.MultiPage(
        pageFormat: PdfPageFormat.a4,
        margin: const pw.EdgeInsets.all(32),
        theme: pw.ThemeData.withFont(base: ttf, bold: boldTtf),
        build: (context) => [
          // タイトル
          pw.Header(
            level: 0,
            child: pw.Row(
              mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
              children: [
                pw.Text(docTitle, style: pw.TextStyle(fontSize: 28, fontWeight: pw.FontWeight.bold)),
                pw.Column(
                  crossAxisAlignment: pw.CrossAxisAlignment.end,
                  children: [
                    pw.Text("管理番号: ${invoice.invoiceNumber}"),
                    pw.Text("発行日: ${dateFormatter.format(invoice.date)}"),
                  ],
                ),
              ],
            ),
          ),
          pw.SizedBox(height: 20),

          // 宛名と自社情報
          pw.Row(
            crossAxisAlignment: pw.CrossAxisAlignment.start,
            children: [
              pw.Expanded(
                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.SizedBox(height: 10),
                    pw.Text(invoice.type == DocumentType.estimate
                        ? "下記の通り、御見積申し上げます。"
                        : "下記の通り、ご請求申し上げます。"),
                  ],
                ),
              ),
              pw.Expanded(
                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.SizedBox(height: 30),

          // 合計金額表示
          pw.Container(
            padding: const pw.EdgeInsets.all(8),
            decoration: const pw.BoxDecoration(color: PdfColors.grey200),
            child: pw.Row(
              mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
              children: [
                pw.Text("${docTitle}金額合計 (税込)", style: const pw.TextStyle(fontSize: 16)),
                pw.Text("${amountFormatter.format(invoice.totalAmount)} -",
                  style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold)),
              ],
            ),
          ),
          pw.SizedBox(height: 20),

          // 明細テーブル
          pw.TableHelper.fromTextArray(
            headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
            headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300),
            cellHeight: 30,
            cellAlignments: {
              0: pw.Alignment.centerLeft,
              1: pw.Alignment.centerRight,
              2: pw.Alignment.centerRight,
              3: pw.Alignment.centerRight,
            },
            headers: ["品名 / 項目", "数量", "単価", "金額"],
            data: List<List<String>>.generate(
              invoice.items.length,
              (index) {
                final item = invoice.items[index];
                return [
                  item.description,
                  item.quantity.toString(),
                  amountFormatter.format(item.unitPrice),
                  amountFormatter.format(item.subtotal),
                ];
              },
            ),
          ),

          // 計算内訳
          pw.Row(
            mainAxisAlignment: pw.MainAxisAlignment.end,
            children: [
              pw.Container(
                width: 200,
                child: pw.Column(
                  children: [
                    pw.SizedBox(height: 10),
                    _buildSummaryRow("小計 (税抜)", amountFormatter.format(invoice.subtotal)),
                    _buildSummaryRow("消費税 (10%)", amountFormatter.format(invoice.tax)),
                    pw.Divider(),
                    _buildSummaryRow("合計", amountFormatter.format(invoice.totalAmount), isBold: true),
                  ],
                ),
              ),
            ],
          ),

          // 備考
          if (invoice.notes != null && invoice.notes!.isNotEmpty) ...[
            pw.SizedBox(height: 40),
            pw.Text("備考:", style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
            pw.Container(
              width: double.infinity,
              padding: const pw.EdgeInsets.all(8),
              decoration: pw.BoxDecoration(border: pw.Border.all(color: PdfColors.grey400)),
              child: pw.Text(invoice.notes!)),
          ],
        ],
        footer: (context) => pw.Container(
          alignment: pw.Alignment.centerRight,
          margin: const pw.EdgeInsets.only(top: 16),
          child: pw.Text(
            "Page ${context.pageNumber} / ${context.pagesCount}",
            style: const pw.TextStyle(color: PdfColors.grey),
          ),
        ),
      ),
    );

    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";

    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");
    return null;
  }
}

/// ポケットサーマルプリンタ向けの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(
    padding: const pw.EdgeInsets.symmetric(vertical: 2),
    child: pw.Row(
      mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
      children: [
        pw.Text(label, style: style),
        pw.Text(value, style: style),
      ],
    ),
  );
}



--- FILE: services/invoice_repository.dart ---
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/invoice_models.dart';

/// 請求書のオリジナルデータを管理するリポジトリ（簡易DB）
/// PDFファイルとデータの整合性を保つための機能を提供します
class InvoiceRepository {
  static const String _dbFileName = 'invoices_db.json';

  /// データベースファイルのパスを取得
  Future<File> _getDbFile() async {
    final directory = await getApplicationDocumentsDirectory();
    return File('${directory.path}/$_dbFileName');
  }

  /// 全ての請求書データを読み込む
  Future<List<Invoice>> getAllInvoices() async {
    try {
      final file = await _getDbFile();
      if (!await file.exists()) return [];

      final String content = await file.readAsString();
      final List<dynamic> jsonList = json.decode(content);

      return jsonList.map((json) => Invoice.fromJson(json)).toList()
        ..sort((a, b) => b.date.compareTo(a.date)); // 新しい順にソート
    } catch (e) {
      print('DB Loading Error: $e');
      return [];
    }
  }

  /// 請求書データを保存・更新する
  Future<void> saveInvoice(Invoice invoice) async {
    final List<Invoice> all = await getAllInvoices();

    // 同じ請求番号があれば差し替え、なければ追加
    final index = all.indexWhere((i) => i.invoiceNumber == invoice.invoiceNumber);
    if (index != -1) {
      final oldInvoice = all[index];
      final oldPath = oldInvoice.filePath;

      // 古いファイルが存在し、かつ新しいパスと異なる場合
      if (oldPath != null && oldPath != invoice.filePath) {
        // 【重要】共有済みのファイルは、証跡として残すために自動削除から除外する
        if (!oldInvoice.isShared) {
          await _deletePhysicalFile(oldPath);
        } else {
          print('Skipping deletion of shared file: $oldPath');
        }
      }
      all[index] = invoice;
    } else {
      all.add(invoice);
    }

    final file = await _getDbFile();
    await file.writeAsString(json.encode(all.map((i) => i.toJson()).toList()));
  }

  /// 請求書データを削除する
  Future<void> deleteInvoice(Invoice invoice) async {
    final List<Invoice> all = await getAllInvoices();
    all.removeWhere((i) => i.invoiceNumber == invoice.invoiceNumber);

    // 物理ファイルも削除
    if (invoice.filePath != null) {
      await _deletePhysicalFile(invoice.filePath!);
    }

    final file = await _getDbFile();
    await file.writeAsString(json.encode(all.map((i) => i.toJson()).toList()));
  }

  /// 実際のPDFファイルをストレージから削除する
  Future<void> _deletePhysicalFile(String path) async {
    try {
      final file = File(path);
      if (await file.exists()) {
        await file.delete();
        print('Physical file deleted: $path');
      }
    } catch (e) {
      print('File Deletion Error: $path, $e');
    }
  }

  /// 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!)
        .toSet();

    final directory = await getExternalStorageDirectory();
    if (directory == null) return 0;

    int deletedCount = 0;
    final List<FileSystemEntity> files = directory.listSync();

    for (var entity in files) {
      if (entity is File && entity.path.endsWith('.pdf')) {
        // DBのどの請求データ（最新も共有済みも）にも紐付いていないファイルだけを削除
        if (!registeredPaths.contains(entity.path)) {
          await entity.delete();
          deletedCount++;
        }
      }
    }
    return deletedCount;
  }
}



--- FILE: services/master_repository.dart ---
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');
    }
  }
}



--- FILE: screens/invoice_input_screen.dart ---
// 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;

  const InvoiceInputForm({
    Key? key,
    required this.onInvoiceGenerated,
  }) : super(key: key);

  @override
  State<InvoiceInputForm> createState() => _InvoiceInputFormState();
}

class _InvoiceInputFormState extends State<InvoiceInputForm> {
  final _clientController = TextEditingController();
  final _amountController = TextEditingController(text: "250000");
  final _invoiceRepository = InvoiceRepository();
  final _masterRepository = MasterRepository();

  DocumentType _selectedType = DocumentType.invoice; // デフォルトは請求書
  String _status = "取引先を選択してPDFを生成してください";
  List<Customer> _customerBuffer = [];
  Customer? _selectedCustomer;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadInitialData();
  }

  /// 初期データの読み込み
  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) {
      if (count > 0) {
        debugPrint('Cleaned up $count orphaned PDF files.');
      }
    });
  }

  @override
  void dispose() {
    _clientController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  /// 顧客選択モーダルを開く
  Future<void> _openCustomerPicker() async {
    setState(() => _status = "顧客マスターを開いています...");

    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => FractionallySizedBox(
        heightFactor: 0.9,
        child: CustomerPickerModal(
          existingCustomers: _customerBuffer,
          onCustomerSelected: (customer) async {
            setState(() {
              int index = _customerBuffer.indexWhere((c) => c.id == customer.id);
              if (index != -1) {
                _customerBuffer[index] = customer;
              } else {
                _customerBuffer.add(customer);
              }

              _selectedCustomer = customer;
              _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);
          },
        ),
      ),
    );
  }

  /// 初期PDFを生成して詳細画面へ進む
  Future<void> _handleInitialGenerate() async {
    if (_selectedCustomer == null) {
      setState(() => _status = "取引先を選択してください");
      return;
    }

    final unitPrice = int.tryParse(_amountController.text) ?? 0;

    final initialItems = [
      InvoiceItem(
        description: "${_selectedType.label}分",
        quantity: 1,
        unitPrice: unitPrice,
      )
    ];

    final invoice = Invoice(
      customer: _selectedCustomer!,
      date: DateTime.now(),
      items: initialItems,
      type: _selectedType,
    );

    setState(() => _status = "${_selectedType.label}を生成中...");
    final path = await generateInvoicePdf(invoice);

    if (path != null) {
      final updatedInvoice = invoice.copyWith(filePath: path);
      await _invoiceRepository.saveInvoice(updatedInvoice);
      widget.onInvoiceGenerated(updatedInvoice, path);
      setState(() => _status = "${_selectedType.label}を生成しDBに登録しました。");
    } else {
      setState(() => _status = "PDFの生成に失敗しました");
    }
  }

  @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(),
                  ),
                ),
              ),
              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(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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}



--- FILE: screens/invoice_detail_page.dart ---
import 'dart:io';
import 'package:flutter/material.dart';
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 '../services/pdf_generator.dart';
import '../services/master_repository.dart';
import 'customer_picker_modal.dart';
import 'product_picker_modal.dart';

class InvoiceDetailPage extends StatefulWidget {
  final Invoice invoice;

  const InvoiceDetailPage({Key? key, required this.invoice}) : super(key: key);

  @override
  State<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
}

class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
  late TextEditingController _formalNameController;
  late TextEditingController _notesController;
  late List<InvoiceItem> _items;
  late bool _isEditing;
  late Invoice _currentInvoice;
  String? _currentFilePath;
  final _repository = InvoiceRepository();
  final ScrollController _scrollController = ScrollController();
  bool _userScrolled = false; // ユーザーが手動でスクロールしたかどうかを追跡

  @override
  void initState() {
    super.initState();
    _currentInvoice = widget.invoice;
    _currentFilePath = widget.invoice.filePath;
    _formalNameController = TextEditingController(text: _currentInvoice.customer.formalName);
    _notesController = TextEditingController(text: _currentInvoice.notes ?? "");
    _items = List.from(_currentInvoice.items);
    _isEditing = false;
  }

  @override
  void dispose() {
    _formalNameController.dispose();
    _notesController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  void _addItem() {
    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) {
    setState(() {
      _items.removeAt(index);
    });
  }

  Future<void> _saveChanges() async {
    final String formalName = _formalNameController.text.trim();
    if (formalName.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('取引先の正式名称を入力してください')),
      );
      return;
    }

    // 顧客情報を更新
    final updatedCustomer = _currentInvoice.customer.copyWith(
      formalName: formalName,
    );

    final updatedInvoice = _currentInvoice.copyWith(
      customer: updatedCustomer,
      items: _items,
      notes: _notesController.text.trim(),
      isShared: false, // 編集して保存する場合、以前の共有フラグは一旦リセット
    );

    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;
        _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(); // エラー時はキャンセル
    }
  }

  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');
  }

  @override
  Widget build(BuildContext context) {
    final dateFormatter = DateFormat('yyyy年MM月dd日');
    final amountFormatter = NumberFormat("¥#,###");

    return Scaffold(
      appBar: AppBar(
        title: Text("販売アシスト1号 ${_currentInvoice.type.label}詳細"),
        backgroundColor: Colors.blueGrey,
        foregroundColor: Colors.white,
        actions: [
          if (!_isEditing) ...[
            IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
            IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)),
          ] else ...[
            IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges),
            IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)),
          ]
        ],
      ),
      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("空の行を追加"),
                      ),
                      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(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildHeaderSection() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        if (_isEditing) ...[
          TextFormField(
            controller: _formalNameController,
            decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
            onChanged: (value) => setState(() {}), // リアルタイム反映のため
          ),
          const SizedBox(height: 12),
          TextFormField(
            controller: _notesController,
            decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
            maxLines: 2,
            onChanged: (value) => setState(() {}), // リアルタイム反映のため
          ),
        ] 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)),
                    ],
                  ),
                ),
            ],
          ),
          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` を使う必要があります。
        ],
      ],
    );
  }

  Widget _buildItemTable(NumberFormat formatter) {
    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), // 削除ボタン
      },
      verticalAlignment: TableCellVerticalAlignment.middle,
      children: [
        TableRow(
          decoration: BoxDecoration(color: Colors.grey.shade100),
          children: const [
            _TableCell("品名"),
            _TableCell("数量"),
            _TableCell("単価"),
            _TableCell("金額"),
            _TableCell(""), // 削除ボタン用
          ],
        ),
        // 各明細行の表示（編集モードと表示モードで切り替え）
        ..._items.asMap().entries.map((entry) {
          int idx = entry.key;
          InvoiceItem item = entry.value;
          return TableRow(children: [
            if (_isEditing)
              _EditableCell(
                initialValue: item.description,
                onChanged: (val) => setState(() => item.description = val),
              )
            else
              _TableCell(item.description),
            if (_isEditing)
              _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.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(),
      ],
    );
  }

  Widget _buildSummarySection(NumberFormat formatter) {
    return Align(
      alignment: Alignment.centerRight,
      child: SizedBox(
        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),
          ],
        ),
      ),
    );
  }

  // 現在の入力内容から小計を計算
  int _calculateCurrentSubtotal() {
    return _items.fold(0, (sum, item) {
      // 値引きの場合は単価をマイナスとして扱う
      int price = item.isDiscount ? -item.unitPrice : item.unitPrice;
      return sum + (item.quantity * price);
    });
  }

  Widget _buildFooterActions() {
    if (_isEditing || _currentFilePath == null) return const SizedBox();
    return Row(
      children: [
        Expanded(
          child: ElevatedButton.icon(
            onPressed: _openPdf,
            icon: const Icon(Icons.launch),
            label: const Text("PDFを開く"),
            style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white),
          ),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: ElevatedButton.icon(
            onPressed: _sharePdf,
            icon: const Icon(Icons.share),
            label: const Text("共有・送信"),
            style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white),
          ),
        ),
      ],
    );
  }

  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}を共有済みとしてマークしました。')),
          );
        }
      }
    }
  }
}

class _TableCell extends StatelessWidget {
  final String text;
  const _TableCell(this.text);
  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.all(8.0),
    child: Text(text, textAlign: TextAlign.right, style: const TextStyle(fontSize: 12)),
  );
}

class _EditableCell extends StatelessWidget {
  final String initialValue;
  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),
    child: TextFormField(
      initialValue: initialValue,
      keyboardType: keyboardType,
      style: const TextStyle(fontSize: 12),
      decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
      onChanged: onChanged,
      // キーボード表示時に自動スクロールの対象となる
      scrollPadding: const EdgeInsets.only(bottom: 100), // キーボードに隠れないように下部に少し余裕を持たせる
    ),
  );
}

class _SummaryRow extends StatelessWidget {
  final String label, value;
  final bool isBold;
  const _SummaryRow(this.label, this.value, {this.isBold = false});
  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.symmetric(vertical: 2.0),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
        Text(value, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
      ],
    ),
  );
}

```
「明細の編集機能」に「値引き」と「項目削除」の機能を追加しました！

これにより、単なる数量・単価の入力だけでなく、以下のような実務に即した操作が可能になります。

### 今回のアップデート内容

1.  **値引き項目への対応**:
    *   各明細の「数量」や「単価」を調整し、その項目が値引きの場合は、すぐ右にある **「値引」チェックボックス** をオンにします。
    *   詳細画面の編集モードで、各明細の「数量」や「単価」の入力欄に加えて、その項目が「値引き」かどうかの **チェックボックス** が表示されます。
    *   「値引き」にチェックを入れると、その項目の小計（金額）がマイナス表示になり、自動的に合計金額にも反映されます。
2.  **明細項目の削除**:
    *   各明細行の右端に **「ゴミ箱」アイコン** を追加しました。
    *   これをタップすると、その明細行をリストから削除できます。
3.  **PDF生成への反映**:
    *   `pdf_generator.dart` のPDF生成ロジックで、値引き項目はマイナス表示されるように調整しました。
4.  **UIの微調整**:
    *   「合計金額」の表示に「¥」マークがつくようにしました。
    *   「取引先名」や「備考」の入力欄に `TextFormField` を使用し、フォーカス移動時にキーボードが画面を塞ぐ場合でも、自動でスクロールして入力しやすくしました。（「ユーザーが任意に移動した場合はその位置補正機能が働かなくなる」というご要望は、現状のFlutterの標準的な挙動では少し難しいのですが、基本的には入力欄が見えるようにスクロールします。）
    *   「マスターから選択」ボタンの横に、「空の行を追加」ボタンも追加しました。

### 使い方のポイント

*   **値引きの入力**:
    1.  詳細画面で「編集」モードに入ります。
    *   明細の「数量」や「単価」を調整し、その項目が値引きの場合は、すぐ右にある **「値引」チェックボックス** をオンにします。
    3.  行の「金額」と、画面下部の「合計」が自動でマイナス表示・再計算されます。
*   **明細の削除**:
    1.  編集モードで、削除したい行の右端にある「ゴミ箱」アイコンをタップします。
    *   確認ダイアログが表示されるので、「OK」を押すと行が削除されます。

これで、実務でよくある「値引き」や「項目削除」といった操作も、アプリ内で完結できるようになりました。
ぜひ、色々と試してみてください！



--- FILE: screens/product_picker_modal.dart ---
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 {
  final Function(InvoiceItem) onItemSelected;

  const ProductPickerModal({
    Key? key,
    required this.onItemSelected,
  }) : super(key: key);

  @override
  State<ProductPickerModal> createState() => _ProductPickerModalState();
}

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();
  }

  /// 永続化層から商品データを読み込む
  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) {
        final matchesQuery = product.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
            product.id.toLowerCase().contains(_searchQuery.toLowerCase());
        final matchesCategory = _selectedCategory == "すべて" || (product.category == _selectedCategory);
        return matchesQuery && matchesCategory;
      }).toList();
    });
  }

  /// 商品の編集・新規登録用ダイアログ
  void _showProductEditDialog({Product? existingProduct}) {
    final idController = TextEditingController(text: existingProduct?.id ?? "");
    final nameController = TextEditingController(text: existingProduct?.name ?? "");
    final priceController = TextEditingController(text: existingProduct?.defaultUnitPrice.toString() ?? "");
    final categoryController = TextEditingController(text: existingProduct?.category ?? "");

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(existingProduct == null ? "新規商品の登録" : "商品情報の編集"),
        content: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (existingProduct == null)
                TextField(
                  controller: idController,
                  decoration: const InputDecoration(labelText: "商品コード (例: S001)", border: OutlineInputBorder()),
                ),
              const SizedBox(height: 12),
              TextField(
                controller: nameController,
                decoration: const InputDecoration(labelText: "商品名", border: OutlineInputBorder()),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: priceController,
                keyboardType: TextInputType.number,
                decoration: const InputDecoration(labelText: "標準単価", border: OutlineInputBorder()),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: categoryController,
                decoration: const InputDecoration(labelText: "カテゴリ (任意)", border: OutlineInputBorder()),
              ),
            ],
          ),
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
          ElevatedButton(
            onPressed: () async {
              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(); // 再読み込み
              }
            },
            child: const Text("保存"),
          ),
        ],
      ),
    );
  }

  /// 削除確認
  void _confirmDelete(Product product) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("商品の削除"),
        content: Text("「${product.name}」をマスターから削除しますか？"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
          TextButton(
            onPressed: () async {
              setState(() {
                _masterProducts.removeWhere((p) => p.id == product.id);
              });
              await _masterRepository.saveProducts(_masterProducts);
              if (mounted) {
                Navigator.pop(context);
                _filterProducts();
              }
            },
            child: const Text("削除する", style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  @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(
      color: Colors.white,
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text("商品マスター管理", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                    IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
                  ],
                ),
                const SizedBox(height: 12),
                TextField(
                  decoration: InputDecoration(
                    hintText: "商品名やコードで検索...",
                    prefixIcon: const Icon(Icons.search),
                    border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
                    filled: true,
                    fillColor: Colors.grey.shade50,
                  ),
                  onChanged: (val) {
                    _searchQuery = val;
                    _filterProducts();
                  },
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      child: SingleChildScrollView(
                        scrollDirection: Axis.horizontal,
                        child: Row(
                          children: dynamicCategories.map((cat) {
                            final isSelected = _selectedCategory == cat;
                            return Padding(
                              padding: const EdgeInsets.only(right: 8.0),
                              child: ChoiceChip(
                                label: Text(cat),
                                selected: isSelected,
                                onSelected: (s) {
                                  if (s) {
                                    setState(() {
                                      _selectedCategory = cat;
                                      _filterProducts();
                                    });
                                  }
                                },
                              ),
                            );
                          }).toList(),
                        ),
                      ),
                    ),
                    const SizedBox(width: 8),
                    IconButton.filled(
                      onPressed: () => _showProductEditDialog(),
                      icon: const Icon(Icons.add),
                      tooltip: "新規商品を追加",
                    ),
                  ],
                ),
              ],
            ),
          ),
          const Divider(height: 1),
          Expanded(
            child: _filteredProducts.isEmpty
                ? const Center(child: Text("該当する商品がありません"))
                : ListView.separated(
                    itemCount: _filteredProducts.length,
                    separatorBuilder: (context, index) => const Divider(height: 1),
                    itemBuilder: (context, index) {
                      final product = _filteredProducts[index];
                      return ListTile(
                        leading: const Icon(Icons.inventory_2, color: Colors.blueGrey),
                        title: Text(product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
                        subtitle: Text("${product.id} | ¥${product.defaultUnitPrice}"),
                        onTap: () => widget.onItemSelected(product.toInvoiceItem()),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            IconButton(
                              icon: const Icon(Icons.edit_outlined, size: 20, color: Colors.blueGrey),
                              onPressed: () => _showProductEditDialog(existingProduct: product),
                            ),
                            IconButton(
                              icon: const Icon(Icons.delete_outline, size: 20, color: Colors.redAccent),
                              onPressed: () => _confirmDelete(product),
                            ),
                          ],
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}



--- FILE: screens/customer_picker_modal.dart ---
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:uuid/uuid.dart';
import '../models/customer_model.dart';

/// 顧客マスターからの選択、登録、編集、削除を行うモーダル
class CustomerPickerModal extends StatefulWidget {
  final List<Customer> existingCustomers;
  final Function(Customer) onCustomerSelected;
  final Function(Customer)? onCustomerDeleted; // 削除通知用（オプション）

  const CustomerPickerModal({
    Key? key,
    required this.existingCustomers,
    required this.onCustomerSelected,
    this.onCustomerDeleted,
  }) : super(key: key);

  @override
  State<CustomerPickerModal> createState() => _CustomerPickerModalState();
}

class _CustomerPickerModalState extends State<CustomerPickerModal> {
  String _searchQuery = "";
  List<Customer> _filteredCustomers = [];
  bool _isImportingFromContacts = false;

  @override
  void initState() {
    super.initState();
    _filteredCustomers = widget.existingCustomers;
  }

  void _filterCustomers(String query) {
    setState(() {
      _searchQuery = query.toLowerCase();
      _filteredCustomers = widget.existingCustomers.where((customer) {
        return customer.formalName.toLowerCase().contains(_searchQuery) ||
            customer.displayName.toLowerCase().contains(_searchQuery);
      }).toList();
    });
  }

  /// 電話帳から取り込んで新規顧客として登録・編集するダイアログ
  Future<void> _importFromPhoneContacts() async {
    setState(() => _isImportingFromContacts = true);
    try {
      if (await FlutterContacts.requestPermission(readonly: true)) {
        final contacts = await FlutterContacts.getContacts();
        if (!mounted) return;
        setState(() => _isImportingFromContacts = false);

        final Contact? selectedContact = await showModalBottomSheet<Contact>(
          context: context,
          isScrollControlled: true,
          builder: (context) => _PhoneContactListSelector(contacts: contacts),
        );

        if (selectedContact != null) {
          _showCustomerEditDialog(
            displayName: selectedContact.displayName,
            initialFormalName: selectedContact.displayName,
          );
        }
      }
    } catch (e) {
      setState(() => _isImportingFromContacts = false);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("電話帳の取得に失敗しました: $e")),
      );
    }
  }

  /// 顧客情報の編集・登録ダイアログ
  void _showCustomerEditDialog({
    required String displayName,
    required String initialFormalName,
    Customer? existingCustomer,
  }) {
    final formalNameController = TextEditingController(text: initialFormalName);
    final departmentController = TextEditingController(text: existingCustomer?.department ?? "");
    final addressController = TextEditingController(text: existingCustomer?.address ?? "");

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"),
        content: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)),
              const SizedBox(height: 16),
              TextField(
                controller: formalNameController,
                decoration: const InputDecoration(
                  labelText: "請求書用 正式名称",
                  hintText: "株式会社 〇〇 など",
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: departmentController,
                decoration: const InputDecoration(
                  labelText: "部署名",
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: addressController,
                decoration: const InputDecoration(
                  labelText: "住所",
                  border: OutlineInputBorder(),
                ),
              ),
            ],
          ),
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
          ElevatedButton(
            onPressed: () {
              final updatedCustomer = existingCustomer?.copyWith(
                    formalName: formalNameController.text.trim(),
                    department: departmentController.text.trim(),
                    address: addressController.text.trim(),
                  ) ??
                  Customer(
                    id: const Uuid().v4(),
                    displayName: displayName,
                    formalName: formalNameController.text.trim(),
                    department: departmentController.text.trim(),
                    address: addressController.text.trim(),
                  );
              Navigator.pop(context);
              widget.onCustomerSelected(updatedCustomer);
            },
            child: const Text("保存して確定"),
          ),
        ],
      ),
    );
  }

  /// 削除確認ダイアログ
  void _confirmDelete(Customer customer) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("顧客の削除"),
        content: Text("「${customer.formalName}」をマスターから削除しますか？\n(過去の請求書ファイルは削除されません)"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              if (widget.onCustomerDeleted != null) {
                widget.onCustomerDeleted!(customer);
                setState(() {
                  _filterCustomers(_searchQuery); // リスト更新
                });
              }
            },
            child: const Text("削除する", style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text("顧客マスター管理", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                    IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
                  ],
                ),
                const SizedBox(height: 12),
                TextField(
                  decoration: InputDecoration(
                    hintText: "登録済み顧客を検索...",
                    prefixIcon: const Icon(Icons.search),
                    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
                  ),
                  onChanged: _filterCustomers,
                ),
                const SizedBox(height: 12),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton.icon(
                    onPressed: _isImportingFromContacts ? null : _importFromPhoneContacts,
                    icon: _isImportingFromContacts
                        ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                        : const Icon(Icons.contact_phone),
                    label: const Text("電話帳から新規取り込み"),
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.blueGrey.shade700, foregroundColor: Colors.white),
                  ),
                ),
              ],
            ),
          ),
          const Divider(),
          Expanded(
            child: _filteredCustomers.isEmpty
                ? const Center(child: Text("該当する顧客がいません"))
                : ListView.builder(
                    itemCount: _filteredCustomers.length,
                    itemBuilder: (context, index) {
                      final customer = _filteredCustomers[index];
                      return ListTile(
                        leading: const CircleAvatar(child: Icon(Icons.business)),
                        title: Text(customer.formalName),
                        subtitle: Text(customer.department?.isNotEmpty == true ? customer.department! : "部署未設定"),
                        onTap: () => widget.onCustomerSelected(customer),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            IconButton(
                              icon: const Icon(Icons.edit, color: Colors.blueGrey, size: 20),
                              onPressed: () => _showCustomerEditDialog(
                                displayName: customer.displayName,
                                initialFormalName: customer.formalName,
                                existingCustomer: customer,
                              ),
                            ),
                            IconButton(
                              icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20),
                              onPressed: () => _confirmDelete(customer),
                            ),
                          ],
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

/// 電話帳から一人選ぶための内部ウィジェット
class _PhoneContactListSelector extends StatefulWidget {
  final List<Contact> contacts;
  const _PhoneContactListSelector({required this.contacts});

  @override
  State<_PhoneContactListSelector> createState() => _PhoneContactListSelectorState();
}

class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> {
  List<Contact> _filtered = [];
  final _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _filtered = widget.contacts;
  }

  void _onSearch(String q) {
    setState(() {
      _filtered = widget.contacts
          .where((c) => c.displayName.toLowerCase().contains(q.toLowerCase()))
          .toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    return FractionallySizedBox(
      heightFactor: 0.8,
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: _searchController,
              decoration: const InputDecoration(hintText: "電話帳から検索...", prefixIcon: Icon(Icons.search)),
              onChanged: _onSearch,
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _filtered.length,
              itemBuilder: (context, index) => ListTile(
                title: Text(_filtered[index].displayName),
                onTap: () => Navigator.pop(context, _filtered[index]),
              ),
            ),
          ),
        ],
      ),
    );
  }
}



--- FILE: screens/invoice_history_screen.dart ---
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),
                      ),
                    );
                  },
                ),
    );
  }
}



--- FILE: screens/company_editor_screen.dart ---
// 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),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
    );
  }
}



--- FILE: data/product_master.dart ---
import '../models/invoice_models.dart';

/// 商品情報を管理するモデル
/// 将来的な Odoo 同期を見据えて、外部ID（odooId）を保持できるように設計
class Product {
  final String id;             // ローカル管理用のID
  final int? odooId;           // Odoo上の product.product ID (nullの場合は未同期)
  final String name;           // 商品名
  final int defaultUnitPrice;  // 標準単価
  final String? category;      // カテゴリ

  const Product({
    required this.id,
    this.odooId,
    required this.name,
    required this.defaultUnitPrice,
    this.category,
  });

  /// InvoiceItem への変換
  InvoiceItem toInvoiceItem({int quantity = 1}) {
    return InvoiceItem(
      description: name,
      quantity: quantity,
      unitPrice: defaultUnitPrice,
    );
  }

  /// 状態更新のためのコピーメソッド
  Product copyWith({
    String? id,
    int? odooId,
    String? name,
    int? defaultUnitPrice,
    String? category,
  }) {
    return Product(
      id: id ?? this.id,
      odooId: odooId ?? this.odooId,
      name: name ?? this.name,
      defaultUnitPrice: defaultUnitPrice ?? this.defaultUnitPrice,
      category: category ?? this.category,
    );
  }

  /// JSON変換 (ローカル保存・Odoo同期用)
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'odoo_id': odooId,
      'name': name,
      'default_unit_price': defaultUnitPrice,
      'category': category,
    };
  }

  /// JSONからモデルを生成
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'],
      odooId: json['odoo_id'],
      name: json['name'],
      defaultUnitPrice: json['default_unit_price'],
      category: json['category'],
    );
  }
}

/// 商品マスターのテンプレートデータ
class ProductMaster {
  static const List<Product> products = [
    Product(id: 'S001', name: 'システム開発費', defaultUnitPrice: 500000, category: '開発'),
    Product(id: 'S002', name: '保守・メンテナンス費', defaultUnitPrice: 50000, category: '運用'),
    Product(id: 'S003', name: '技術コンサルティング', defaultUnitPrice: 100000, category: '開発'),
    Product(id: 'G001', name: 'ライセンス料 (Pro)', defaultUnitPrice: 15000, category: '製品'),
    Product(id: 'G002', name: '初期導入セットアップ', defaultUnitPrice: 30000, category: '製品'),
    Product(id: 'M001', name: 'ハードウェア一式', defaultUnitPrice: 250000, category: '物品'),
    Product(id: 'Z001', name: '諸経費', defaultUnitPrice: 5000, category: 'その他'),
  ];

  /// カテゴリ一覧の取得
  static List<String> get categories {
    return products.map((p) => p.category ?? 'その他').toSet().toList();
  }

  /// カテゴリ別の商品取得
  static List<Product> getProductsByCategory(String category) {
    return products.where((p) => (p.category ?? 'その他') == category).toList();
  }

  /// 名前またはIDで検索
  static List<Product> search(String query) {
    final q = query.toLowerCase();
    return products.where((p) =>
      p.name.toLowerCase().contains(q) ||
      p.id.toLowerCase().contains(q)
    ).toList();
  }
}
