Next.js で作る EC 管理ダッシュボード: 注文、返金、在庫
Next.js のストア管理画面の難しい部分: 2 つの注文ステータス、一括操作の部分的な失敗、セント単位で合う返金計算、在庫の調整、割引。
Preet Suthar · · 6分で読めます
- Next.js
- React
- E-commerce
- Dashboard
EC の管理画面は CRUD アプリのように見えます。注文のテーブル、注文ごとのページ、商品ごとのフォーム。テーブルは簡単な部分です。難しいのは、ストア管理画面のすべての数字がお金か在庫であり、そのどちらも、裏で顧客が買い続けている間ずっと正しくなければならないことです。
ここでは、Next.js でストアの管理側を作って学んだことを紹介します。正直なステータスを持つ注文テーブル、残っている作業を示す注文ページ、セント単位で合う返金、ライブの注文に耐える在庫編集、そしてマーチャントが読める割引です。
注文: ステータスは 1 つではなく 2 つ
最初の間違いは、status 列を 1 つにすることです。注文には少なくとも 2 つの独立した状態があります。支払い(保留中、オーソリ済み、支払い済み、一部返金済み、返金済み)と発送(未発送、一部発送済み、発送済み)です。「支払い済みかつ未発送」はマーチャントが毎朝片付けるリストですが、ステータス列が 1 つではそれを表現できません。
2 つのピルとして表示します。意味を色だけに担わせないでください。WCAG は情報を示す手段が色だけにならないことを求めていますし、緑の「支払い済み」の隣にある薄いオレンジの「一部支払い済み」は、多くの人にとって見分けにくいものです。テキストラベルに小さなグリフ(空、半分、満の円)を添えれば、一目で読めます。
保存済みビューは URL に置く
マーチャントは注文を眺めるのではなく、リストを片付けていきます。未発送、未払い、未完了。保存済みビューはフィルター、並び順、検索にすぎません。クエリ全体を URL に入れておけば、リンクで同じリストが開き、戻るボタンも機能します。Next.js ではそれをサーバーで読み取ります。
現在のフィルターが保存済みビューと異なるときは、タブに変更済みの印を付け、「リセット」「ビューを更新」「新しいビューとして保存」を提示します。スプレッドシートがフィルターがかかっていることを知らせるのと同じです。
新しい注文でカーソルの下の行を動かさない
マーチャントがリストを見ている間にも注文は入ってきます。新しい行がすべてを押し下げると、クリックしようとしていたチェックボックスが別の注文のものになってしまいます。行にフォーカスがあるか何かが選択されている間は、新しい注文を「新規注文 1 件 · 表示」のピルの奥に保留し、何も進行中でないときだけ上部に挿入します。
一括操作は部分的に失敗する
「6 件の注文を発送済みにする」が 5 件しか処理しないことがあります。1 件は保留中か、1 分前にキャンセルされたのです。それを前提に API を設計してください。失敗した ID をそれぞれの理由とともに返し、その内容をそのまま伝えます。
そうすればメッセージは自然に決まります。「5 件の注文を発送済みにしました。1 件は処理できませんでした: #1031 は保留中です」とし、その注文へのリンクを付けます。全部成功か全部失敗かのエラートーストでは、どれが通ったのかマーチャントは推測するしかありません。
注文ページには残っている作業を表示する
注文ページでは、商品を買われ方ではなく、その後どうなったかで分けます。発送ごとに配送業者と追跡番号付きのカードを用意します。返金された商品にも専用のカードを。そして最も重要なのは、まだ発送が残っているもののカードです。
その下のタイムラインは注文のイベントから組み立てます。データから作った短いタイトル(「$564.41 の支払いを確定」、追跡番号のリンク付きの「UPS で発送」)、ツールチップに正確な時刻、同じ種類の繰り返しイベントは 1 行に畳みます。スタッフのコメントも同じフィードに並び、楽観的に投稿され、失敗したら再試行できます。
返金の計算はセント単位で合わなければならない
返金は、管理画面のコードが最もよくお金を間違える場所です。顧客が注文全体への $10 割引と税金付きで 3 つの商品を買い、1 つを返品しました。いくら返金されるべきでしょうか?
注文単位の金額は、各行の価格に比例して行に配分します。割引は価格で、税は割引後の価格で。整数のセントで分けると剰余が出ますが、最大剰余法でそれを配れば、各部分の合計は常に元の金額と一致します。
allocate(1000, [3000, 3000, 3000]) は [334, 333, 333] を返し、合計はぴったり 1000 になります。
罠はもう 1 つあります。ある行の 1 個を今日返金し、残りの 2 個を来週返金した場合、その合計は 3 個を一度に返金した場合と一致しなければなりません。返金ごとに個別に丸めるのではなく、各単位に行の取り分の累積的な一切れを割り当てます。
from はすでに返金された個数です。返金をどう分けても、各切れの合計は lineShare になります。
返金画面の残りは状態の扱いです。送料は、支払われた額から返金済みの額を引いた分まで返金できます。注文がギフトカードとカードに分けて支払われていた場合、お金は決まった順序で支払い方法に戻ります。冪等キーを送って再試行されたリクエストで二重に返金されないようにし、高額の返金では確認を求め、「銀行で処理中」は成功や失敗ではなく独立した状態として表示します。
在庫: 上書きせず、増減で調整する
素朴な在庫エディターは新しい数字を送ります: 「Sand / Large は 47」。マーチャントが入力している間に 2 件の注文が入っていたら、47 を保存するとそれが黙って取り消されてしまいます。
代わりに、変更を調整として送ります。「5 個入荷」は差分であり、差分ならその間に何が起きていても安全に適用できます。棚卸し後の「47 に設定」のように、マーチャントが本当に数量そのものを意図している場合は、それを見えていた数量に対する差分に変換し、その数量が動いていたらサーバーに拒否させます。
拒否された行は「別のデバイスで数量が 40 に変更されました」のようなメッセージとともに下書きに残り、「再適用」で新しい数字の上にもう一度送ります。Shopify の inventoryAdjustQuantities ミューテーションも、changeFromQuantity フィールドで同じ仕組みになっています。
数字も分けてください。手持ち在庫は建物の中にあるものですが、引当済みの在庫はすでに売れていて、利用不可の在庫は破損しているか保留中です。マーチャントには手持ちとは別に販売可能数を見せる必要があります。そうしないと、すでに売れた在庫を売ってしまいます。
割引を文として書く
割引のフォームはたいていフィールドの長いリストで、顧客から苦情が来るまでマーチャントは自分が何を作ったのかわかりません。オファーを「お客様は Ring Vase と Glass Carafe が 15% オフ」のように編集可能な部分を含む 1 つの文として書き、最低購入金額、対象顧客、利用回数の上限、併用、スケジュールは現在の値を表示する折りたたまれた行に入れ、割引を適用したチェックアウトのサンプルをプレビューします。
利用回数の上限がない 100% オフのような危険な設定には警告を出し、スケジュールはブラウザではなくストアのタイムゾーンで設定します。
1 つのストア、すべてのページ
最後のピースは一貫性です。商品を返金したら、注文の支払いステータス、タイムライン、顧客の累計購入額、在庫に戻した数量がすべて一緒に変わるべきです。すべてのミューテーションに変更したエンティティを返させ、それを表示しているすべてのビューを更新または再検証します。そして、保存されていない商品・在庫・割引の編集があるときは、ブラウザの戻るボタンも含めて画面遷移をブロックします。
これを実装した HextaUI のブロック
HextaUI Pro の管理画面ブロックは、こうした問題を中心に作られています。Orders Table には URL に保持される保存済みビュー、進捗グリフ付きの支払いと発送の別々のピル、部分的な失敗を報告する一括操作があります。Refund は計算を calculateRefund(order, selection) で行います。これは最大剰余法を使う、テスト付きの純粋関数です。Inventory は上書きではなく調整を保存し、拒否された行を再適用できます。Discount Builder は、ライブのチェックアウトプレビューの横でオファーを文として編集します。
Commerce Dashboard は、これらを Order Detail、Product Editor、Sales Overview と合わせて、共有ストアの上で 1 つのストア管理画面にまとめます。デフォルトはハッシュルーティングで、router プロップを通して Next.js のルートも使えます。各アクションを Shopify Admin API に対応付ける例も付いています。ライブプレビューはダッシュボード全体なので、注文を返金してすべてのページが更新されるのを確かめられます。
よくある質問
注文のステータスは 1 つにすべきですか、2 つにすべきですか? 少なくとも 2 つです。支払いと発送は独立していて、マーチャントはその組み合わせで絞り込みます。
返金する商品に割引をどう配分すればいいですか? 整数のセントで、各行の価格に比例して配分し、最大剰余法を使って各部分の合計がぴったり一致するようにします。
在庫の編集では新しい数量と変化量のどちらを送るべきですか? 変化量です。差分なら、マーチャントの編集中に入った注文があっても壊れません。「◯◯ に設定」という数量は、その元になった数量と照合すべきです。
管理画面のフィルターはどこに置くべきですか? URL です。そうすればビューを共有でき、リロードでき、戻るボタンで移動できます。