SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

ComponentZine(ActiveReports)byメシウス(AD)

レポートパーツとDesignerで帳票開発を効率化する

第3回

 前回はFirestoreのデータをActiveReportsJSの帳票にバインドし、WijmoのFlexGridと連携させるパターンを実装しました。最終回となる今回は、ActiveReportsJS Designerをアプリケーションに組み込んで「現場担当者が自分で帳票レイアウトを修正できる」仕組みを実現します。さらに、レポートパーツやマスターレポートによる帳票開発の効率化と、PDF/Excel/HTMLエクスポートについても解説します。

対象読者

  • Next.jsで業務システムを開発している方
  • 帳票機能の組み込みを検討している方
  • ActiveReportsJSやWijmoに興味がある方

前提環境

 筆者の検証環境は以下の通りです。

  • macOS Tahoe 26.3.1
  • Node.js 25.2.1
  • npm 11.6.2
  • Next.js 16.1.3
  • React 19.2.3
  • Wijmo 5.20252.44
  • ActiveReportsJS 6.0.1
  • Firebase 12.8.0

はじめに

 前回までに、ActiveReportsJS Viewerで帳票を表示し、Wijmoコンポーネントからデータを連携させる仕組みを構築しました。しかし、帳票のレイアウトは開発時に作り込んだものが使われており、変更が必要になるたびに開発者への修正依頼が発生する状態でした。

 「請求書のタイトルを変えたい」「列の幅を調整したい」といったレイアウトの微調整のたびに開発サイクルを回すのは、業務の現場では非効率です。ActiveReportsJS Designerをアプリケーションに組み込むことで、現場担当者がブラウザ上で帳票レイアウトを直接編集・保存できるようになります。

 図1は、今回構築するDesigner画面の完成イメージです。

図1:Next.jsに組み込まれたActiveReportsJS Designer
図1:Next.jsに組み込まれたActiveReportsJS Designer

 今回は以下の内容を解説します。

  1. ActiveReportsJS Designerの組み込みと保存機能の実装
  2. レポートパーツによる共通部品の作成と再利用
  3. マスターレポートによる帳票テンプレートの管理
  4. PDF/Excel/HTMLへのプログラマティックなエクスポート

ActiveReportsJS Designerの組み込み

 ActiveReportsJS Designerは、ブラウザ上で帳票レイアウトを視覚的に編集できるコンポーネントです。前回までに組み込んだViewerと同様に、Next.jsアプリケーションに埋め込むことができます。

Designerコンポーネントの実装

 Viewerの組み込みと同様に、Designerもブラウザ環境でのみ動作するため、SSR無効化と動的インポートが必要です。ここでは、より柔軟に初期化処理を制御できるよう、@mescius/activereportsjs/reportdesignerから直接インスタンス化する方法を採用します(リスト1)。

[リスト1]src/components/reports/ReportDesigner.tsx(Designer初期化部分)
"use client";

import { useEffect, useRef, useState } from "react";
import "@mescius/activereportsjs/styles/ar-js-ui.css";
import "@mescius/activereportsjs/styles/ar-js-designer.css";

export default function ReportDesigner({ reportUri, masterReportId }) {
  const hostRef = useRef(null);
  const designerRef = useRef(null);

  useEffect(() => {
    const initDesigner = async () => {
      if (!hostRef.current) return;

      // 日本語ロケール
      await import("@mescius/activereportsjs-i18n");
      await import("@mescius/activereportsjs-i18n/dist/designer/ja-locale.js");

      // Designer を動的インポートして初期化
      const ArDesigner = await import(
        "@mescius/activereportsjs/reportdesigner"
      );
      designerRef.current = new ArDesigner.Designer(hostRef.current);

      // レポートを開く
      if (reportUri) {
        await designerRef.current.setReport({ id: reportUri });
      } else {
        await designerRef.current.createReport({ reportType: "CPL" });
      }
    };
    initDesigner();

    return () => {
      designerRef.current?.destroy?.();
      designerRef.current = null;
    };
  }, [reportUri, masterReportId]);

  return <div ref={hostRef} style={{ width: "100%", height: "700px" }} />;
}

 Viewerの場合と同じくuseEffect内で動的インポートを行いますが、CSSファイルが異なる点に注意してください。Viewerではar-js-viewer.cssを使いましたが、Designerではar-js-designer.cssを使用します。ar-js-ui.cssは共通です。

 日本語化には、Viewer用の@mescius/activereportsjs-i18nに加えて、Designer固有のdesigner/ja-locale.jsも必要です。setReport({ id: reportUri })で既存のレポート定義ファイルを開き、createReport({ reportType: "CPL" })で新規レポートを作成します。

保存機能の実装

 Designerで編集したレポートを保存するには、setActionHandlersonSave/onSaveAsコールバックを設定します(リスト2)。

[リスト2]src/components/reports/ReportDesigner.tsx(保存ハンドラー部分)
// 保存ハンドラーを設定
designer.setActionHandlers({
  // 上書き保存:現在のレポートIDでサーバーに送信
  onSave: async (info) => {
    const res = await fetch("/api/reports", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: info.id,                // レポートのファイルパス
        definition: info.definition, // レポート定義JSON
      }),
    });
    if (!res.ok) throw new Error("保存に失敗しました");
    // 保存成功時は表示名を返す
    return { displayName: info.displayName };
  },
  // 名前を付けて保存:ユーザーに新しいファイル名を入力してもらう
  onSaveAs: async (info) => {
    const name = prompt("ファイル名を入力してください");
    if (!name) return undefined; // キャンセル時は何もしない
    const res = await fetch("/api/reports", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: /reports/${name},
        definition: info.definition,
      }),
    });
    if (!res.ok) throw new Error("保存に失敗しました");
    // 新しいIDと表示名を返してDesignerに反映
    return { id: /reports/${name}, displayName: name };
  },
});

 onSaveは上書き保存、onSaveAsは名前を付けて保存に対応します。info.definitionにはレポート定義のJSONオブジェクトが渡されるため、これをサーバー側のAPIに送信してファイルとして保存します。

レポート保存用API Route

 保存先としてNext.jsのAPI Routeを用意します(リスト3)。

[リスト3]src/app/api/reports/route.ts
import { NextRequest, NextResponse } from "next/server";
import { writeFile } from "fs/promises";
import { join } from "path";

export async function POST(request: NextRequest) {
  // リクエストボディからレポートIDと定義を取得
  const { id, definition } = await request.json();

  // 必須パラメータのバリデーション
  if (!id || !definition) {
    return NextResponse.json(
      { error: "id and definition are required" },
      { status: 400 }
    );
  }

  // ファイル名のサニタイズ(パストラバーサル防止)
  const filename = id.replace(/^\/reports\//, "").replace(/[^a-zA-Z0-9._-]/g, "");
  const filePath = join(process.cwd(), "public", "reports", filename);
  // 読みやすいようにインデント付きでJSON文字列化
  const json = JSON.stringify(definition, null, 2);

  // ファイルとして保存
  await writeFile(filePath, json, "utf-8");

  return NextResponse.json({ success: true, path: /reports/${filename} });
}

 受け取ったレポート定義JSONをpublic/reports/ディレクトリにファイルとして保存します。ファイル名のサニタイズでは、まずreplace(/^\/reports\//, "")でパスプレフィックスを除去してファイル名だけを取り出し、次にreplace(/[^a-zA-Z0-9._-]/g, "")で英数字・ドット・ハイフン・アンダースコア以外の文字を除去しています。これにより../などを使ったパストラバーサル攻撃を防いでいます。

 JSON.stringify(definition, null, 2)の第2引数nullはreplacer関数を省略する指定で全プロパティがそのまま出力されます。第3引数2はインデント幅で、保存されたJSONファイルを人間が読みやすい形式にします。

 本番環境ではデータベースやクラウドストレージへの保存が適切ですが、開発・デモ用途としてはこのシンプルな構成で十分です。

Designerページの構成

 Designer用のページは、前回のViewer組み込みと同じく、Server Component + Client Component + dynamic({ ssr: false })のパターンで構成します(リスト4)。

[リスト4]src/app/designer/page.tsx / DesignerClient.tsx
// page.tsx(Server Component)― レイアウトとSuspense境界を定義
import { Suspense } from "react";
import DesignerClient from "./DesignerClient";

export default function DesignerPage() {
  return (
    <div>
      <h1>帳票デザイナー</h1>
      <Suspense>
        <DesignerClient />
      </Suspense>
    </div>
  );
}

// DesignerClient.tsx(Client Component)― SSR無効でDesignerを読み込む
"use client";
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";

// Designerはブラウザ専用のため、SSRを無効化して動的インポート
const ReportDesigner = dynamic(
  () => import("@/components/reports/ReportDesigner"),
  { ssr: false }
);

export default function DesignerClient() {
  // URLクエリパラメータから編集対象のレポートURIを取得
  const searchParams = useSearchParams();
  const reportUri = searchParams.get("report") || undefined;
  return <ReportDesigner reportUri={reportUri} />;
}

 useSearchParamsでURLクエリパラメータを取得し、?report=/reports/order-detail.rdlx-jsonのように既存レポートを指定してDesignerで開くことができます。

帳票の編集と保存を試す

 Designerの組み込みが完了したところで、実際に帳票を編集してみましょう。前回作成した受発注明細レポート(order-detail.rdlx-json)をDesignerで開き、タイトルを「受発注明細書」から「請求書」に変更して、別名で保存します。

 Designerでは、テキストボックスをダブルクリックするとテキストを直接編集できます。編集後、ツールバーの保存ボタンから「名前を付けて保存」を選ぶと、ファイル名を入力するダイアログが表示されます。ここではinvoice.rdlx-jsonとして保存しました。

 図2は、タイトルを「請求書」に変更して保存する操作の画面です。

図2:Designerでタイトルを変更して保存
図2:Designerでタイトルを変更して保存

 このように、開発者でなくてもブラウザ上で帳票レイアウトを修正し、保存できる仕組みが実現しました。

レポートパーツで共通部品を作成する

 帳票の種類が増えると、会社名・住所・ロゴといった共通要素を帳票ごとに作り直す問題が出てきます。ActiveReportsJSのレポートパーツ機能を使うと、これらの共通要素をライブラリ化して再利用できます。

レポートパーツの作成手順

 レポートパーツは、Designer上で作成します。共通ヘッダー(会社名・住所・電話番号)を例に、作成手順を説明します。

  1. Designerで新規レポートを作成する
  2. TextBoxを配置して、会社名(16pt)、住所(9pt)、電話番号(9pt)を縦に並べる
  3. 配置した要素をすべて選択し、右クリックメニューから「レポートパーツの作成」を選ぶ
  4. 右ペインの「レポートパーツ」タブで表示名を設定する
  5. 「名前を付けて保存」でライブラリファイルとして保存する

 図3は、レポートパーツを作成した後のDesigner画面です。右ペインに「レポートパーツ」タブが表示され、パーツの表示名を設定できます。

図3:Designerでレポートパーツを作成
図3:Designerでレポートパーツを作成

ライブラリの読み込み

 保存したレポートパーツライブラリは、Designerの初期化時にsetReportPartsLibrariesで読み込みます(リスト5)。

[リスト5]src/components/reports/ReportDesigner.tsx(レポートパーツライブラリ設定)
// レポートパーツライブラリを設定(ファイルが存在する場合のみ)
// HEADリクエストでライブラリファイルの存在を確認
const libRes = await fetch("/reports/parts-library.rdlx-json",
  { method: "HEAD" });
// ファイルが存在する場合のみライブラリとして登録
if (libRes.ok) {
  await designer.setReportPartsLibraries([
    {
      id: "/reports/parts-library.rdlx-json",
      name: "CommonParts",
      displayName: "共通パーツ",
    },
  ]);
}

 ライブラリを設定すると、Designerの左パネルの「ライブラリ」タブから、登録されたパーツを帳票上にドラッグ&ドロップで配置できるようになります。共通ヘッダーを複数の帳票で統一したい場合に便利です。

 図4は、Designerの「ライブラリ」タブを開いた状態です。「共通パーツ」の下に登録されたパーツが一覧表示され、帳票上にドラッグ&ドロップで配置できます。

図4:Designerのライブラリタブに共通パーツが表示された状態
図4:Designerのライブラリタブに共通パーツが表示された状態

マスターレポートで帳票テンプレートを管理する

 レポートパーツが「部品の再利用」を実現する機能だとすると、マスターレポートは「テンプレートの共通化」を実現する機能です。マスターレポートを使うと、共通レイアウトをベースに新規帳票を効率的に作成できます。

マスターレポートへの変換

 既存のレポートをマスターレポートに変換するには、Designerの「レポート」タブから「マスターレポートに変換」を選びます。変換後、ツールバーに「エリアの追加」ボタンが表示され、レポートエリア(コンテンツプレースホルダ)を定義できるようになります。

 図5は、受発注明細レポートをマスターレポートに変換した画面です。

図5:マスターレポートに変換した画面
図5:マスターレポートに変換した画面

マスターレポートの保存と利用

 マスターレポートを保存する際は、拡張子を.rdlx-json-masterにする必要があります。通常の.rdlx-jsonで保存すると、派生レポート作成時にDecodingErrorが発生します。

 マスターレポートをベースに新規レポートを作成するには、createReportmasterReportIdを指定します(リスト6)。

[リスト6]マスターレポートベースの新規レポート作成
// マスターレポートをベースにした新規レポートを作成
await designer.createReport({
  reportType: "CPL",
  masterReportId: "/reports/invoice-template.rdlx-json-master",
});

 派生レポートでは、マスターレポートのレイアウトが読み取り専用で表示され、レポートエリア内のみ編集可能になります。請求書・納品書・注文書のように共通レイアウトを持つ帳票群を管理する際に有効です。

 図6は、マスターレポートをベースに作成した派生レポートをDesignerで開いた状態です。右側のプロパティパネルに「マスターレポート」フィールドが表示され、マスター部分がグレーアウトして読み取り専用になっていることが確認できます。

図6:派生レポートをDesignerで開いた状態
図6:派生レポートをDesignerで開いた状態

PDF/Excel/HTMLエクスポート

 ActiveReportsJSでは、Viewer UIを使わずにプログラマティックに帳票をエクスポートできます。PDF・Excel・HTMLの3形式に対応しており、それぞれ専用モジュールを動的インポートして使用します(リスト7)。

[リスト7]src/components/reports/ExportButtons.tsx(エクスポート処理)
const handleExport = async (format: "pdf" | "xlsx" | "html", filename: string) => {
  // ActiveReportsJSのコアモジュールを動的インポート
  const Core = await import("@mescius/activereportsjs/core");

  const pageReport = new Core.PageReport();

  // データバインディングが必要な場合はレポート定義を加工して読み込む
  if (data) {
    // レポート定義JSONを取得
    const response = await fetch(reportUri);
    const reportDef = await response.json();
    // データソースの接続文字列にJSONデータを直接埋め込む
    if (reportDef.DataSources && reportDef.DataSources.length > 0) {
      reportDef.DataSources[0].ConnectionProperties.ConnectString =
        "jsondata=" + JSON.stringify(data);
    }
    // 加工済みの定義オブジェクトを読み込む
    await pageReport.load(reportDef);
  } else {
    // データ不要の場合はURIから直接読み込む
    await pageReport.load(reportUri);
  }

  // レポートを実行してページドキュメントを生成
  const pageDocument = await pageReport.run();

  // 指定された形式でエクスポートしてダウンロード
  if (format === "pdf") {
    const PdfExport = await import("@mescius/activereportsjs/pdfexport");
    const result = await PdfExport.exportDocument(pageDocument, {
      info: { title: filename },
    });
    result.download(${filename}.pdf);
  } else if (format === "xlsx") {
    const XlsxExport = await import("@mescius/activereportsjs/xlsxexport");
    const result = await XlsxExport.exportDocument(pageDocument);
    result.download(${filename}.xlsx);
  } else {
    const HtmlExport = await import("@mescius/activereportsjs/htmlexport");
    const result = await HtmlExport.exportDocument(pageDocument);
    result.download(${filename}.html);
  }
};

 エクスポートの流れは、PageReportでレポートを読み込み → run()で実行 → 各形式のexportDocument()でエクスポート → download()でダウンロード、という4ステップです。

 if (data)ブロックでは、プログラマティック実行時にデータを帳票に注入する処理を行っています。Viewerで表示する場合はデータソース設定に基づいてデータが自動的に取得されますが、PageReportで直接実行する場合は、レポート定義JSONを取得したうえでデータソースのConnectString"jsondata=" + JSON.stringify(data)を設定し、実行時データを埋め込む必要があります。

 図7は、エクスポートボタンを配置した画面です。

図7:PDF/Excel/HTMLエクスポートボタン
図7:PDF/Excel/HTMLエクスポートボタン

 前節でDesignerを使って編集・保存した請求書レポートも、このエクスポート機能を通じてPDFやExcelとして出力できます。Designerでレイアウトを調整し、エクスポートで配布用ファイルを生成する、という一連の業務フローがアプリケーション内で完結します。

まとめ

 本記事では、ActiveReportsJS Designerの組み込みを中心に、帳票開発を効率化する機能を解説しました。

  • Designer組み込み: @mescius/activereportsjs/reportdesignerから直接インスタンス化し、setActionHandlersで保存機能を実装
  • レポートパーツ: 共通のヘッダー・フッターをライブラリ化し、setReportPartsLibrariesで複数帳票から再利用
  • マスターレポート: 共通レイアウトをテンプレート化し、.rdlx-json-masterとして管理
  • エクスポート: PageReportrun()exportDocument()でPDF/Excel/HTMLをプログラマティックに生成

 全3回のシリーズを通じて、Firebase + Next.js + Wijmo + ActiveReportsJSによる業務システムの構築手順を解説してきました。

 最終的に構築したシステムでは、開発者がWijmoとActiveReportsJSで業務UIと帳票の基盤を作り、現場担当者がDesignerで帳票レイアウトを自分で調整できる、という役割分担が実現しています。本番運用に向けては、レポート定義の永続化先(データベースやクラウドストレージ)の検討や、Designerの編集権限管理などが追加で必要になるでしょう。

 サンプルコードは本記事の添付ファイルからダウンロードできます。ぜひ手元で動かして、帳票開発の効率化を体験してみてください。

この記事は参考になりましたか?

提供:メシウス株式会社

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/24010 2026/06/23 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング