1. Top
  2. ブログ一覧
  3. Shopify Admin blockで管理画面に好きなコンテンツを追加しよう
Eコマース

Shopify Admin blockで管理画面に好きなコンテンツを追加しよう

公開日

2024.12.18

Shopify Admin blockで管理画面に好きなコンテンツを追加しようのサムネイル

Shopifyはストア運営者向けに、柔軟な拡張性とカスタマイズ性を提供し続けています。テーマカスタマイズや拡張機能(アプリ)を通じて、顧客向けのフロントエンドページを思い通りに設計したり、バックエンドロジックを強化したりすることが可能です。その一方で、管理者が日常的に操作する「Shopify Admin」(管理画面)においても、独自の表示や機能を追加したいケースがあるかもしれません。

たとえば、セールやキャンペーンの実施状況を常時確認できるダッシュボードを表示したり、オペレーター向けに特定商品のタグ付けや在庫処理を効率化するためのショートカットを用意したりと、ストア管理の生産性向上につながる数々の施策が考えられます。こうしたニーズに応える手段として、Shopify Admin blockが登場しました。

Shopify Admin blockとは

Shopify Admin blockとは、Shopifyの管理画面(Admin)に対して独自のUIコンポーネントや機能を差し込むための拡張機能です。従来、Admin画面のカスタマイズは限定的でしたが、Admin blockを利用することで、管理者向けの新たなインターフェースを構築したり、特定のオペレーションを簡略化するためのボタンやフォーム、データ表示などを直接管理画面上に組み込むことが可能になります。

これにより、Shopifyストアオーナーやスタッフは、わざわざ外部ツールや別ページを行き来する必要がなくなり、日常業務の効率化が期待できます。Admin blockを通じて、ビジネスロジックに即したカスタムUIを提供できるため、チーム内のワークフロー改善やオペレーションの自動化にも役立ちます。

Admin blockの作成方法

今回は商品詳細画面に直近1週間の売上データを表示するAdmin blockを作成する手順を紹介します。

要件

作成するにあたって以下の要件が必要です。

  • アプリのスコープ
    • 商品情報を取得するのでread_productsスコープが必要です。
    • 注文情報を取得するのでread_ordersスコープが必要です。

read_ordersについてはパートナーダッシュボードからの設定が必要となります。Shopify Admin print actionを使って管理画面に印刷機能を追加する方法でも設定方法が解説されているので参考にしてください。

extensionの作成

Shopify CLI を使用して、ブロック拡張機能のスターターコードを生成します。

shopify app generate extension --template admin_block --name product-sales-report --flavor react

このコマンドは、アプリのextensionsディレクトリに次の構造を持つ新しい拡張テンプレートを作成します。

extensions/product-sales-report
├── README.md
├── locales
│   ├── en.default.json // 拡張機能のデフォルトの英語言語翻訳ファイル
│   └── fr.json // フランス語言語翻訳ファイル
├── package.json
├── shopify.extension.toml // 拡張機能の設定ファイル
└── src
    └── BlockExtension.jsx // ブロック内容を定義するコード

拡張機能のUIを書く

構成を確認します。

shopify.extension.tomlには、拡張機能の静的構成が保存されます。製品の詳細ページに問題追跡ツールを表示するには、ターゲットをadmin.product-details.block.renderに設定します。

api_version = "2024-01"

[[extensions]]
name = "t:name"
handle = "product-sales-report"
type = "ui_extension"
[[extensions.targeting]]
module = "./src/BlockExtension.jsx"
target = "admin.product-details.block.render"

ターゲットは商品詳細画面以外に注文・コレクション画面でも埋め込むことができます。詳細はAdmin block locationsを参照してください。

続いてタイトルを更新します。 locales/en.default.jsonを編集して、拡張機能の表示名を設定します。

{
  "name": "Product sales report"
}

フランス語は一旦使用しないので、locales/fr.jsonlocales/ja.jsonに変更して、日本語の表示名を設定します。

{
  "name": "商品売上レポート"
}

コンポーネントを使用して拡張機能のUIを作成します。src/BlockExtension.jsxを編集して、次のようにします。
今回はサンプルなのでシンプルな箇条書きのリストを表示します。

import { useEffect, useState } from "react";
import {
  reactExtension,
  useApi,
  AdminBlock,
  BlockStack,
  Text,
  Box,
  Paragraph,
  InlineStack,
  ProgressIndicator,
} from "@shopify/ui-extensions-react/admin";
import { getProductSkus, getSalesData } from "./utils";

const TARGET = "admin.product-details.block.render";

export default reactExtension(TARGET, () => <SalesReportApp />);

function SalesReportApp() {
  const { data } = useApi(TARGET);
  const [loading, setLoading] = useState(true);
  const [salesData, setSalesData] = useState([]);
  const [error, setError] = useState(null);

  const productId = data?.selected?.[0]?.id;

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);

        if (!productId) {
          throw new Error("商品が選択されていません。");
        }

        // 商品のSKUを取得
        const skuResponse = await getProductSkus(productId);
        const skus =
          skuResponse?.data?.product?.variants?.edges?.map(
            (node) => node.node.sku,
          ) || [];

        if (skus.length === 0) {
          setSalesData([]); // SKUがない場合、売上データも空
        }

        // SKUに基づく売上データを取得
        const salesResponse = await getSalesData(skus);
        const salesByDate = {};

        salesResponse?.data?.orders?.edges?.forEach(({ node }) => {
          const date = new Date(node.createdAt).toLocaleDateString("ja-JP");
          const amount = parseFloat(node.totalPriceSet.shopMoney.amount);
          salesByDate[date] = (salesByDate[date] || 0) + amount;
        });

        const formattedSales = Object.entries(salesByDate)
          .map(([date, sales]) => ({ date, sales }))
          .sort((a, b) => new Date(b.date) - new Date(a.date));

        setSalesData(formattedSales);
      } catch (err) {
        setError(err.message || "データの取得中にエラーが発生しました");
        console.error("Error:", err);
      } finally {
        setLoading(false); // ローディング終了
      }
    }

    // 初期化処理を開始
    fetchData();
  }, [productId]); // productId の変更時にのみ実行

  if (loading) {
    return (
      <InlineStack blockAlignment="center" inlineAlignment="center">
        <ProgressIndicator size="large-100" />
      </InlineStack>
    );
  }

  if (error) {
    return (
      <AdminBlock title="1週間の売り上げレポート">
        <Text tone="critical">エラーが発生しました: {error}</Text>
      </AdminBlock>
    );
  }

  return (
    <AdminBlock title="1週間の売り上げレポート">
      <Text fontWeight="bold">商品売上をクイックに確認できます💪</Text>
      <BlockStack spacing="base">
        {salesData.length === 0 ? (
          <Box padding="base">
            <Text>この期間の売上データはありません。</Text>
          </Box>
        ) : (
          salesData.map((data) => (
            <Box padding="base" key={data.date}>
              <Paragraph><Text fontWeight="bold">{data.date}</Text> の売り上げ:{" "}
                <Text fontWeight="bold">
                  {data.sales.toLocaleString("ja-JP")}
                </Text>{" "}
                円
              </Paragraph>
            </Box>
          ))
        )}
      </BlockStack>
    </AdminBlock>
  );
}

このUIに必要なデータを渡せるよう、src/utils.jsを作成して、商品のSKUと売上データを取得する関数を定義します。 Admin GraphQL APIを使用します。

export async function getProductSkus(productId) {
  return await makeGraphQLQuery(
    `query Product($id: ID!) {
      product(id: $id) {
        id
        # 本来はvariantsCountを取得して、その数だけvariantsを取得するのが確実です
        variants(first: 10) {
          edges {
            node {
              sku
            }
          }
        }
      }
    }`,
    { id: productId },
  );
}

// skuで指定された商品の週間売上を取得する
export async function getSalesData(skus) {
  const today = new Date();
  const lastWeek = new Date(today);
  lastWeek.setDate(today.getDate() - 7);

  const lastWeekISO = lastWeek.toISOString();

  const skuQuery = skus.map((sku) => `sku:${sku}`).join(" OR ");

  return await makeGraphQLQuery(
    `query Orders($query: String!) {
    # 本来はページネーションを使用して全データを取得するのが確実です
      orders(query: $query first: 100) {
        edges {
          node {
            name
            totalPriceSet {
              shopMoney {
                amount
              }
            }
            createdAt
          }
        }
      }
    }`,
    {
      query: `${skuQuery} created_at:>=${lastWeekISO}`,
    },
  );
}

async function makeGraphQLQuery(query, variables) {
  const graphQLQuery = {
    query,
    variables,
  };

  const res = await fetch("shopify:admin/api/graphql.json", {
    method: "POST",
    body: JSON.stringify(graphQLQuery),
  });

  if (!res.ok) {
    console.error("Network error");
  }

  return await res.json();
}

動作確認

開発サーバーを起動して、Admin blockが正常に表示されるか確認します。

shopify app dev

商品詳細画面の下部に「1週間の売り上げレポート」というタイトルのAdmin blockが表示され、直近1週間の売上データがリスト表示されることを確認します。

alt text

売り上げがない場合は「この期間の売上データはありません。」と表示されます。

alt text

もし画面をもっと凝るのでしたら、種類のセレクターや日付の範囲指定など、機能を拡張してみてください。

注意点

Admin blockはあくまで補足的な情報を表示するためのものです。情報量の多い高さのあるコンポーネントを表示すると警告が出て隠れてしまいます。 埋め込みアプリで表示するように移動するなどの対応してください。

alt text

まとめ

この記事では、Shopify Admin blockを使って、管理画面に独自のコンテンツを追加する方法を解説しました。業務効率化やカスタマイズ性向上のための具体的な実装手順を紹介しました。Shopify Admin blockを活用して、ぜひ管理画面をカスタマイズしてみてください。

参考