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でタイトルを変更して保存

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

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

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

ComponentZine(ActiveReports)byメシウス連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 中川 幸哉(ナカガワ ユキヤ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

提供:メシウス株式会社

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

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

この記事をシェア

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

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング