対象読者
- Next.jsで業務システムを開発している方
- 帳票機能の組み込みを検討している方
- WijmoやActiveReportsJSに興味がある方
前提環境
筆者の検証環境は以下の通りです。
- macOS Tahoe 16.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
はじめに
業務システムの開発において、データの一覧表示や帳票の作成・確認は重要な要素です。Reactベースのフロントエンドでは、Wijmoが提供する豊富なUIコンポーネントを活用することで、効率的に業務アプリケーションを構築できます。また、帳票機能にはActiveReportsJSを組み合わせることで、柔軟なレイアウト設計と現場での編集が可能です。この組み合わせにより、見た目の品質を担保しつつ、開発現場はビジネスロジックに集中できます。
本連載では、メシウスが提供する高機能UIコントロールを多数備えたJavaScript開発ライブラリ「Wijmo」と、JavaScript帳票ライブラリである「ActiveReportsJS」を軸に、業務システムの基盤構築から帳票連携までのステップを解説します。アプリケーションフレームワークとしては、ReactでのデファクトスタンダードとなっているNext.jsを採用します。データベースとしてはサンプルにFirebase Firestoreを採用していますが、Firestore特有の機能は使いませんので、現場では任意のデータベースを利用して構いません。連載の全体像としては、次の順序で解説します。
- WijmoとActiveReportsJSを組み合わせた業務システムの基盤構築(本記事)
- Firestoreデータとの連携とWijmoからActiveReportsJSへのデータフローの実装
- レポートパーツやマスターレポートを活用した帳票の部品化と、ActiveReportsJS Designerによる現場での帳票編集機能の実現
本記事では、第1回としてNext.jsアプリケーションにWijmoとActiveReportsJS Viewerを組み込み、仮データを使ってUIの表示ができることを目指します。
プロジェクト構成の概要
今回構築する受発注管理システムは、商品・取引先・商品分類のマスタ管理と、受発注データの管理を行います。主要なデータモデルとして、Product(商品)、Partner(取引先)、Category(商品分類)、Order(受発注)を定義しています(リスト1)。
export interface Product {
id: string; // FirestoreドキュメントID
code: string; // 商品コード(表示用)
name: string;
categoryId: string; // 商品分類への参照
price: number;
stock: number;
unit: string; // 単位(個、箱、kgなど)
…中略…
}
export interface Partner {/* 省略 */}
export interface Category {/* 省略 */}
export interface Order {
id: string;
orderNumber: string; // 伝票番号
type: "purchase" | "sales"; // 発注 or 受注
partnerId: string;
partnerName: string; // 非正規化した取引先名
…中略…
items: OrderItem[]; // 明細行の配列
totalAmount: number;
…中略…
}
これらのデータモデルは、Firestoreのドキュメント構造に対応しています。partnerNameのように一部のフィールドに非正規化した値を持たせることで、一覧表示時のクエリを効率化しています。
Wijmoの組み込み
WijmoをNext.jsに組み込む際の最大のポイントは、SSR(サーバーサイドレンダリング)の無効化です。Wijmoはクライアント側で実行されることを前提としたライブラリのため、サーバー側で実行するとエラーになります。"use client"ディレクティブとdynamic関数を組み合わせて対応します(リスト2)。
"use client"; // クライアントコンポーネントとして宣言
import dynamic from "next/dynamic";
const ProductsGrid = dynamic(
() => import("@/components/wijmo/ProductsGrid"),
{ ssr: false } // サーバーサイドレンダリングを無効化
);
この2段階のアプローチ("use client"とdynamic import)により、Wijmoコンポーネントはブラウザでのみロードされるようになります。ライセンス設定は、環境変数から読み込んでsetLicenseKey()で適用します(リスト3)。NEXT_PUBLIC_プレフィックスにより、クライアントサイドからアクセス可能になります。
"use client";
import { setLicenseKey } from "@mescius/wijmo";
// 環境変数からライセンスキーを取得
const licenseKey = process.env.NEXT_PUBLIC_WIJMO_LICENSE_KEY;
if (licenseKey) {
setLicenseKey(licenseKey); // アプリケーション全体に適用
}
このコンポーネントをルートレイアウトで読み込むことで、すべてのWijmoコンポーネントにライセンスが適用されます。評価版の場合はこの設定を省略できます。
Wijmoは多言語対応しており、日本語カルチャを適用することで日本語表示が可能になります。言語設定は「カルチャ」と呼ばれており、日本語向けの設定はコンポーネントで@mescius/wijmo.cultures/wijmo.culture.jaをインポートするだけで適用されます。
業務アプリ向けUIコンポーネント
Wijmoには業務システム向けのUIコンポーネントが豊富に用意されています。ここでは、本サンプルアプリケーションで使用する4つのコンポーネントを紹介します。ダウンロードサンプルでは、それぞれ以下のパスで実装を確認できます。
-
FlexGrid(商品一覧):
src/components/wijmo/ProductsGrid.tsx -
MultiRow(受発注入力):
src/components/wijmo/OrdersMultiRow.tsx -
TreeView(商品分類):
src/components/wijmo/CategoriesTreeView.tsx -
Accordion(管理メニュー):
src/components/wijmo/AdminAccordion.tsx
FlexGridはExcelライクな操作感を持つデータグリッド、MultiRowは1レコードを複数行で表現する伝票形式のグリッド、TreeViewは階層構造のツリー表示、Accordionは折りたたみ可能なパネルUIです。各コンポーネントの詳しいプロパティ設定やカスタマイズについては、次回以降Firestoreとの連携を進める中で随時解説します。
FlexGrid:商品一覧
FlexGridは、Excelライクな操作感を持つデータグリッドです。ソート、フィルタ、インライン編集といった機能を標準で備えています。データソースにはWijmoのCollectionViewを使用します。CollectionViewは配列データをラップし、ソート・フィルタ・変更追跡といった機能をグリッドに提供するオブジェクトです。
リスト4では、useEffect内でCollectionViewのインスタンス(cv)を生成し、setView(cv)でReactのstate変数viewにセットしています。FlexGridのitemsSourceプロパティにはこのviewをバインドすることで、Reactのライフサイクルに沿ったデータ管理が実現できます(リスト4)。
import { useState, useEffect } from "react";
import { FlexGrid, FlexGridColumn } from "@mescius/wijmo.react.grid";
import { FlexGridFilter } from "@mescius/wijmo.react.grid.filter";
import { CollectionView } from "@mescius/wijmo";
const [view, setView] = useState<CollectionView | null>(null);
useEffect(() => {
// CollectionViewでデータソースを作成
const cv = new CollectionView(products, {
trackChanges: true, // データの変更を追跡
});
setView(cv); // state変数viewにセット
}, [products]);
…中略…
<FlexGrid
itemsSource={view} // CollectionViewをバインド
alternatingRowStep={1} // 1行おきに背景色を変える
…中略…
>
{/* フィルタ機能を追加 */}
<FlexGridFilter />
{/* カラム定義 */}
<FlexGridColumn binding="code" header="商品コード" width={100} />
{/* widthに*を指定することで、残り幅を自動調整 */}
<FlexGridColumn binding="name" header="商品名" width="*" />
…中略…
{/* 金額バインディングのとき、format属性で通貨形式を指定できる */}
<FlexGridColumn binding="price" header="単価" format="c0" />
…中略…
</FlexGrid>
FlexGridFilterを子コンポーネントとして配置するだけで、カラムヘッダーにフィルタ機能が追加されます。bindingプロパティでデータソースのフィールド名を、formatプロパティで表示形式を指定します(図1)。
図1のように、各カラムヘッダーにフィルタアイコンが表示されます。カラムヘッダーをクリックするとソートが、フィルタアイコンをクリックすると条件指定による絞り込みが可能です。これらの機能は追加のコードなしで利用できます。
図2は、商品名カラムのフィルタアイコンをクリックした際に表示されるフィルタダイアログの例です。値フィルタと条件フィルタの2種類が用意されており、チェックボックスの切り替えや条件式の指定で直感的にデータを絞り込めます。
図2:FlexGridのフィルタダイアログ
FlexGridでは、DataMapを使ってセルにコンボボックス形式のカスタムエディタを設定できます。DataMapはキーと表示名のマッピングを提供するオブジェクトで、FlexGridColumnのdataMapプロパティに指定します。これにより、ユーザーはドロップダウンから値を選択でき、内部的にはキー値(ID)が保持されます(リスト5)。
import { DataMap } from "@mescius/wijmo.grid";
// カテゴリマスタからDataMapを作成(idをキー、nameを表示名に)
const categoryMap = useMemo(
() => new DataMap(categories, "id", "name"),
[categories]
);
// 文字列配列を渡すだけでもDataMapとして機能する
const unitOptions = ["個", "箱", "本", "袋", "缶", "切", "冊", "台", "セット", "100g", "kg"];
…中略…
{/* dataMap属性でコンボボックスエディタを設定 */}
<FlexGridColumn binding="categoryId" header="分類" width={120} dataMap={categoryMap} />
<FlexGridColumn binding="unit" header="単位" width={80} dataMap={unitOptions} />
分類列のセルをクリックすると、図3のようにカテゴリ一覧がドロップダウンで表示されます。DataMapによりIDと表示名が自動変換されるため、表示用の変換処理を別途書く必要がありません。
図3:FlexGridのカスタムエディタ(コンボボックス)
FlexGridのカスタムエディタには、コンボボックスの他にもInputMaskによるマスク入力があります。取引先一覧(PartnersGrid.tsx)の電話番号列を例に見てみましょう(リスト6)。
import { InputMask } from "@mescius/wijmo.input";
// InputMaskエディタを作成(数字3桁-4桁-4桁の電話番号マスク)
useEffect(() => {
const editor = new InputMask(document.createElement("div"), {
mask: "000-0000-0000", // 0は数字1桁、ハイフンはリテラル文字
});
setPhoneEditor(editor);
return () => editor.dispose();
}, []);
…中略…
<FlexGridColumn binding="phone" header="電話番号" width={130} editor={phoneEditor} />
InputMaskのmaskプロパティで0は数字1桁を表し、ハイフンはリテラル文字として自動挿入されます。InputMaskもInputDateと同様に@mescius/wijmo.inputパッケージに含まれるコントロールです。
図4:FlexGridのカスタムエディタ(マスク入力)
MultiRow:受発注入力
MultiRowは、1レコードを複数行で表示するコンポーネントです。受発注データのように項目が多い場合に有効です。layoutDefinitionで行のレイアウトを定義します。各グループのcolspanプロパティで、そのグループが何列分の幅を占めるかを指定します。グループ内のcells配列に定義したセルは、colspanの列数に収まるように自動的に複数行に折り返されます。個々のセルにもcolspanを指定でき、備考欄のように複数列にまたがるセルを作れます(リスト7)。
const layoutDefinition = [
{
header: "伝票情報", // グループヘッダー
colspan: 3, // このグループは3列分の幅
cells: [ // 3列×2行に自動配置される
{ binding: "orderNumber", header: "伝票番号", width: 120 },
{ binding: "type", header: "区分", width: 70 },
{ binding: "status", header: "ステータス", width: 90 },
{ binding: "orderDate", header: "発注日", width: 100, format: "yyyy/M/d" },
{ binding: "deliveryDate", header: "納期", width: 100, format: "yyyy/M/d" },
// colspanで2列分の幅を指定
{ binding: "notes", header: "備考", width: 150, colspan: 2 },
],
},
{
header: "取引先・金額",
colspan: 2, // このグループは2列分の幅
cells: [
{ binding: "partnerName", header: "取引先名", width: 180 },
{ binding: "itemCount", header: "明細数", width: 70, align: "right" },
{ binding: "totalAmount", header: "合計金額", width: 120, format: "c0", align: "right" },
],
},
];
…中略…
<MultiRow
itemsSource={view}
layoutDefinition={layoutDefinition} // 複数行レイアウトを適用
…中略…
/>
1レコードが2行で表示され、伝票情報と金額情報が見やすく配置されます(図5)。
通常のグリッドでは横スクロールが必要になる項目数でも、MultiRowなら1画面に収まります。業務システムでよくある「伝票形式」の表示に最適です。
MultiRowでも、FlexGridと同様にDataMapを使ったコンボボックスや、InputDateを使ったカレンダーピッカーをカスタムエディタとして設定できます。layoutDefinitionの各セル定義にdataMapやeditorプロパティを追加するだけで、セルごとに適切な入力UIを提供できます(リスト8)。
import { DataMap } from "@mescius/wijmo.grid";
import { InputDate } from "@mescius/wijmo.input";
// 区分・ステータスのDataMapを定義
const typeDataMap = useMemo(() => new DataMap(
[{ key: "purchase", name: "発注" }, { key: "sales", name: "受注" }],
"key", "name"
), []);
const statusDataMap = useMemo(() => new DataMap([
{ key: "draft", name: "下書き" }, { key: "confirmed", name: "確定" },
{ key: "shipped", name: "出荷済" }, { key: "completed", name: "完了" },
{ key: "cancelled", name: "キャンセル" },
], "key", "name"), []);
// InputDateエディタを作成
useEffect(() => {
const editor = new InputDate(document.createElement("div"), {
format: "yyyy/M/d",
});
setDateEditor(editor);
return () => editor.dispose();
}, []);
// layoutDefinitionのセルにdataMapとeditorを設定
{ binding: "type", header: "区分", width: 70, dataMap: typeDataMap },
{ binding: "status", header: "ステータス", width: 90, dataMap: statusDataMap },
{ binding: "orderDate", header: "発注日", width: 100, format: "yyyy/M/d", editor: dateEditor },
{ binding: "deliveryDate", header: "納期", width: 100, format: "yyyy/M/d", editor: dateEditor },
DataMapにより、内部値("purchase"や"draft")が自動的に表示名(「発注」「下書き」)に変換されるため、表示用のデータ変換処理が不要になります。また、日付列ではカレンダーピッカーから直感的に日付を選択できます(図6)。
TreeView:商品分類
TreeViewは、階層構造のデータを表示するコンポーネントです。まず、表示するデータの構造を確認しましょう。商品分類データは、以下のように親子関係を持つ再帰的なツリー構造で定義されています(リスト9)。
// CategoryTree型: Categoryを継承し、items?: CategoryTree[] を追加した再帰構造
const mockCategoryTree: CategoryTree[] = [
{
id: "cat1",
name: "食品",
…中略…
items: [
{
id: "cat1-1",
name: "生鮮食品",
…中略…
items: [
{ id: "cat1-1-1", name: "野菜", …中略… },
{ id: "cat1-1-2", name: "果物", …中略… },
…中略…
],
},
{ id: "cat1-2", name: "加工食品", …中略…, items: [ …中略… ] },
{ id: "cat1-3", name: "飲料", …中略…, items: [ …中略… ] },
],
},
{ id: "cat2", name: "日用品", …中略…, items: [ …中略… ] },
{ id: "cat3", name: "事務用品", …中略…, items: [ …中略… ] },
];
このツリーデータをTreeViewのdisplayMemberPathとchildItemsPathで表示します。displayMemberPathで表示する項目名(ここではname)、childItemsPathで子要素の配列(ここではitems)を指定します(リスト10)。
<TreeView
itemsSource={categories}
displayMemberPath="name" // 表示するプロパティ名
childItemsPath="items" // 子要素の配列プロパティ名
selectedItemChanged={handleSelectedItemChanged} // 選択変更時のコールバック
…中略…
isAnimated={true} // 展開/折りたたみをアニメーション
/>
階層構造がツリー形式で表示され、クリックで展開・折りたたみができます。図7の左側の表示がTreeViewです。
商品分類のような親子関係を持つデータを、視覚的に分かりやすく表示できます。分類を選択したときに呼び出されるhandleSelectedItemChangedコールバックの実装は以下の通りです(リスト11)。
const [selectedCategory, setSelectedCategory] = useState<CategoryTree | null>(null);
const handleSelectedItemChanged = (sender: TreeView) => {
const item = sender.selectedItem as CategoryTree | null;
setSelectedCategory(item);
};
sender.selectedItemで現在選択されているアイテムを取得し、Reactのstateに保存しています。これにより、図7の右側パネルのように選択した分類の詳細情報(分類ID、分類名、子分類数)を表示できます。このパターンは、マスタ選択UIとして幅広く活用できます。
Accordion:マスタ管理メニュー
Accordionは、折りたたみ可能なメニューを作成するコンポーネントです。まず、メニューデータの構造を見てみましょう(リスト12)。
interface MenuItem {
label: string;
href: string;
description: string;
}
interface MenuSection {
title: string;
icon: string;
items: MenuItem[];
}
const menuSections: MenuSection[] = [
{
title: "マスタ管理",
icon: "📁",
items: [
{ label: "商品マスタ", href: "/products", description: "商品の登録・編集・削除" },
{ label: "取引先マスタ", href: "/partners", description: "取引先の登録・編集・削除" },
{ label: "商品分類マスタ", href: "/categories", description: "商品カテゴリの階層管理" },
],
},
…中略… // 受発注管理、帳票出力、システム設定の3セクションが続く
];
allowExpandManyをfalseにすると、一度に開けるパネルを1つに制限できます(リスト13)。
<Accordion
allowExpandMany={false} // 同時に1つのパネルのみ展開可能
isAnimated={true}
>
{menuSections.map((section) => (
<AccordionPane key={section.title}>
<div>{section.title}</div> {/* パネルヘッダー */}
<ul>{/* メニュー項目 */}</ul> {/* パネルコンテンツ */}
</AccordionPane>
))}
</Accordion>
セクションをクリックすると、該当するメニューが展開されます(図8)。
マスタ管理、受発注管理、帳票出力といった機能カテゴリをAccordionで整理することで、多機能なシステムでもナビゲーションが煩雑になりません。
ActiveReportsJS Viewerの組み込み
ActiveReportsJS ViewerもWijmoと同様、SSR無効化が必要です。また、必須のCSSファイルを2つインポートします(リスト14)。
"use client";
// 必須のスタイルシート
import "@mescius/activereportsjs/styles/ar-js-ui.css";
import "@mescius/activereportsjs/styles/ar-js-viewer.css";
const initViewer = async () => {
// 日本語ロケールを動的インポート
await import("@mescius/activereportsjs-i18n");
// Viewerモジュールを動的インポート
const ArViewer = await import("@mescius/activereportsjs/reportviewer");
// DOM要素にViewerをマウント
viewerRef.current = new ArViewer.Viewer(hostRef.current);
// レポート定義ファイルを開く
await viewerRef.current.open(reportUri);
};
@mescius/activereportsjs-i18nをインポートすることで、ViewerのUIが日本語化されます(図9)。
ViewerのツールバーからPDF出力や印刷が可能です。ページ送り、ズーム、検索といった機能も標準で備わっており、業務帳票の閲覧に必要な機能が揃っています。
まとめ
本記事では、Next.js + Firebase + Wijmo + ActiveReportsJSの4つの技術スタックを導入し、受発注管理システムの基盤を構築しました。SSR無効化のパターンは、両ライブラリで共通して適用できます。次回は、FirestoreのデータをActiveReportsJSにバインドし、WijmoのFlexGridで選択した行を帳票に表示する連携パターンを解説します。

