(続々々々)SVG帳票

[SVG帳票](https://qiita.com/mackey1966/items/0d2a44038b09d22da3f1)もいよいよ仕上げに入る。SVGをファイルのまま管理するには無理があるのでDBで管理する。モデル。

```py
class Template(Base):
    __tablename__ = "svg_templates"

    fid = Column(
        String(50),
        nullable=False,
        index=True
    )
    uid = Column(
        String(50),
        primary_key=True,
        default=generate_uuid,
        index=True
    )
    created_at = Column(
        DateTime(timezone=True),
        server_default=func.utc_timestamp(6), 
        nullable=False
    )
    created_by = Column(
        String(50),
        nullable=False
    )
    updated_at = Column(
        DateTime(timezone=True),
        server_default=func.utc_timestamp(6), 
        onupdate=func.utc_timestamp(6),  
        nullable=False
    )
    updated_by = Column(
        String(50),
        nullable=True
    )
    is_del = Column(
        Boolean,
        nullable=False,
        default=False
    )
    is_active = Column(
        Boolean,
        nullable=False,
        default=True,
        index=True
    )
    is_locked = Column(
        Boolean,
        nullable=False,
        default=False
    )
    name = Column(
        String(255),
        nullable=False
    )
    template_type = Column(
        String(50),
        nullable=False,
        default="common",
        index=True
    )
    category = Column(
        String(50),
        nullable=False,
        index=True
    )
    subcategory = Column(
        String(50),
        nullable=False,
        index=True
    )
    paper_size = Column(
        String(20),
        nullable=True
    )
    landscape = Column(
        Boolean,
        nullable=False,
        default=False
    )
    content = Column(
        Text(length=16777215),
        nullable=False
    )
    remarks = Column(
        String(500),
        nullable=True
    )
    language = Column(
        String(10),
        nullable=True,
        default="en"
    )
    version = Column(
        Integer,
        nullable=False,
        default=1
    )

    __table_args__ = (
        Index('idx_fid_type_category', 'fid', 'template_type', 'category', 'subcategory'),
    )
```
スキーマやCRUD、フロントエンドもClaudeがコードをくれたが省略する（これがなかなか参考になった）。できた画面はこんな感じ。

![スクリーンショット 2025-01-22 20.56.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56191/b91abf07-9e85-24ab-7b11-009792ea5f7a.png)

フロントエンドの印刷コードはDBからテンプレートを探してSVGを読み込み、該当するものがなければファイルを使う。
```js
async loadAndDisplayTemplate() {
    try {
        // controlに値がある場合のみpaperSizeを更新
        if (this.control?.rabies_vac_certificate_size) {
            this.paperSize = this.control.rabies_vac_certificate_size;
        }
        // まずDBからテンプレートの取得を試みる
        const template = await this.getTemplate(this.category, this.subcategory, this.paperSize);
        if (template) {
            // DBのテンプレートが見つかった場合
            this.currentTemplate = template.content;
        } else {
            // DBのテンプレートが見つからなかった場合、rawSvgを使用
            const rawSvgModule = await import("@/svg/certificate_rabies.svg?raw");
            this.currentTemplate = rawSvgModule.default;
        }
        
        // SVGをDOMに追加
        this.$refs.svgContainer.innerHTML = this.currentTemplate;
        
        // SvgPaperで処理
        this.display();
    } catch (error) {
        console.error('Failed to load template:', error);
        try {
            // エラーが発生した場合もrawSvgにフォールバック
            const rawSvgModule = await import("@/svg/certificate.svg?raw");
            this.currentTemplate = rawSvgModule.default;
            this.$refs.svgContainer.innerHTML = this.currentTemplate;
            this.display();
        } catch (fallbackError) {
            console.error('Failed to load fallback template:', fallbackError);
            // ユーザーにエラーを通知
            this.$toast.error(this.$t("Failed to load template"));
        }
    }
},
```
あとは帳票のバリエーションを増やしていくだけ。SVGは帳票の数だけ必要だが、コードの方は同じ帳票なら用紙サイズは以下の感じで対応できる。
```js
.adjustText('#form_title', this.paperSize=="A5" ? 370 : 324, 'middle')
.adjustText('#owner_phone', this.paperSize=="A5" ? 125 : 112, 'middle')
.adjustText('#color', this.paperSize=="A5" ? 158 : 150, 'middle')
.adjustText('#pet_name', this.paperSize=="A5" ? 159 : 150, 'middle')
.adjustText('#birthday', this.paperSize=="A5" ? 180 : 163, 'middle')
.adjustText('#gender', this.paperSize=="A5" ? 180 : 163, 'middle')
.adjustText('#size', this.paperSize=="A5" ? 180 : 163, 'middle')
.adjustText('#rabies_reg_no', this.paperSize=="A5" ? 80 : 76, 'middle')
.adjustText('#rabies_vac_no', this.paperSize=="A5" ? 80 : 76, 'middle')
.adjustText('#impl_date', this.paperSize=="A5" ? 136 : 105, 'middle')
```
プレースホルダの置き換えや位置調整は帳票ごとに必要なので準備は大変だが、一度用意してしまえばあとはFigmaで編集、エクスポート、SVGをDBにペーストして保存、印刷テストの繰り返し。なんとFigmaはSVGをインポートできるのでエンドユーザーに編集してもらうことも可能（方法は違うがFileMakerでやっていたことに近いことができるの）だ。

Webアプリで他社製ツールやサービスを使わずにここまで自由自在に帳票レイアウトを編集できるとは1年前まで思ってもみなかった。何よりこれらのコードをAIに聞いたらホイホイくれることも（ここ1年コーディングはコピペしかしていないように思う）。今は便利に使わせてもらっているがこの先どうなるのか考えると恐ろしい（のであまり考えないようにしている）。