diff --git a/public/sim/serving-robot.html b/public/sim/serving-robot.html
index 547448c..6b69a93 100644
--- a/public/sim/serving-robot.html
+++ b/public/sim/serving-robot.html
@@ -241,0 +242,2 @@ footer { color: var(--ink-3); font-size: 11px; }
+
+ ロボットの費用と変わること(ひと月)は下にあります ↓
@@ -280,0 +283,77 @@ footer { color: var(--ink-3); font-size: 11px; }
+
+ ロボットの費用と、変わること(ひと月)
+
+
+
+
+
+
人件費:店が回る最少のスタッフは何人減るか
+
+
+
人手不足:スタッフの手がふさがっている割合(9割を超えると呼び出しに出られない)/作業の軽さ:スタッフ1人が歩く距離/サービス:入店待ちと、料理ができてから届くまで
+
+
+
+
+
人件費のほかに、ロボットを入れて良くなること
+
+ - 人手不足:配膳の往復をロボットが受け持つ分、スタッフの手が空き、人が足りない日でも案内・会計・呼び出しに出やすくなる(2つめの表の「手がふさがっている割合」)
+ - 作業の軽さ:厨房と客席の往復が減り、歩く距離が短くなる。下げ膳にも使えば、重い皿を運ぶ負担も減る(「1人が歩く距離」)
+ - サービス:料理が早く届き、入店待ちも縮む。空いた手を、客と話す・様子を見るといった人にしかできない接客に回せる(「入店待ち」「料理が届くまで」)
+ - 試算に入れていないこと:採用・教育の手間、人が辞めたときの穴の埋めやすさ、スタッフの疲れ方。数字にはしていませんが、店によっては大きい点です
+
+
+
費用の前提を選ぶ
+
+
+
+
+
+
+
+
+
+
+
+
+ 費用の前提と補助金(2026年10月に調べた値)
+
+ この費用の試算は、上の試算の結果(店が回るかどうか)をそのまま使っています。上の試算の数字は変えていません。人件費は「店が回る最少のスタッフ」がロボットで何人減るかだけから出していて、人手不足・作業の軽さ・サービスの変化は、2つめの表に試算の結果をそのまま並べていて、お金には直していません。
+ まだ入れていないもの:地図づくり・設定・スタッフへの説明にかかる初めの費用、故障で止まっている間の手間、電気代以外の消耗品、補助金の申請の手間と通るかどうか、税金(買ったときの減価償却の扱い)。
+
+
+
@@ -573,0 +653 @@ function render() {
+ renderCost();
@@ -670,0 +751,178 @@ function renderAssume() {
+// ロボットの費用と、浮く人件費(2026-10-23 追加)。上の試算の結果(店が回るか)をそのまま使い、数字は変えない
+// 人件費の差:ロボット n 台で「店が回る最少のスタッフ」が、ロボットなしより何人減るか × 浮く時間 × 時給
+const COST = {
+ pay: "rent", price: { rent: 60000, buy: 2500000 }, maint: 10000, years: 5, sub: false,
+ wage: 1177, hours: "peak", peaks: 2,
+};
+const COST_DAYS = 30; // ひと月の営業日
+const COST_CHARGE = 2000; // 充電の電気代(1台・月)。公開されている目安 1,000〜3,000円の真ん中
+const COST_SHIFT_H = 4; // 「シフト1回分」で浮くと数える時間
+const SUB_RATE = 0.5, SUB_CAP = 7500000; // 省力化投資補助金(カタログ注文型):1/2以下・従業員6〜20人の上限750万円
+const PRICE_OPTS = {
+ rent: [30000, 60000, 100000],
+ buy: [1500000, 2500000, 3500000],
+};
+const man = v => {
+ if (Math.round(v / 1000) === 0) return "0円";
+ const a = Math.abs(v) / 10000, r = Math.round(a * 10) / 10, t = a >= 100 ? Math.round(a).toLocaleString("ja-JP") : (Number.isInteger(r) ? String(r) : r.toFixed(1));
+ return `${v < 0 ? "−" : ""}${t}万円`;
+};
+// 左の条件のままで、ロボット n 台のとき店が回る最少のスタッフ(選べる人数の範囲で探す)
+function minStaff(n) {
+ const s = S(), arr = arrN();
+ for (const st of s.staff_all) {
+ const r = find(state.store, st, n, arr, state.mode);
+ if (r && !unstable(r)) return { st, r };
+ }
+ return null;
+}
+function robotCost(n) {
+ const p = COST.price[COST.pay];
+ const run = n * (COST.maint + COST_CHARGE);
+ if (COST.pay === "rent") return { month: n * p + run, run, first: 0, subsidy: 0 };
+ const total = n * p, subsidy = COST.sub ? Math.min(total * SUB_RATE, SUB_CAP) : 0;
+ const first = total - subsidy;
+ return { month: first / (COST.years * 12) + run, run, first, subsidy };
+}
+function hoursSaved() { return COST.hours === "peak" ? S().peak_min / 60 : COST_SHIFT_H; }
+function buildCost() {
+ const yen = v => v.toLocaleString("ja-JP");
+ seg("seg-cpay", "cpay", [{ v: "rent", l: "月額で借りる" }, { v: "buy", l: "買う" }], () => COST.pay, v => { COST.pay = v; buildCost(); });
+ const po = PRICE_OPTS[COST.pay];
+ seg("seg-cprice", "cprice", po.map(v => ({ v, l: `${v / 10000}万円` })), () => COST.price[COST.pay], v => COST.price[COST.pay] = v);
+ document.getElementById("cprice-legend").textContent = COST.pay === "rent" ? "1台の月額" : "1台の値段";
+ document.getElementById("cprice-note").textContent = COST.pay === "rent"
+ ? "レンタル・リースの相場は月3〜12万円(中央値は6万円台)。"
+ : "買う場合の相場は150〜350万円(中央値は250万円前後)。";
+ seg("seg-cmaint", "cmaint", [{ v: 0, l: "込み" }, { v: 10000, l: "1万円" }, { v: 20000, l: "2万円" }], () => COST.maint, v => COST.maint = v);
+ seg("seg-cyears", "cyears", [3, 5, 7].map(v => ({ v, l: `${v}年` })), () => COST.years, v => COST.years = v);
+ seg("seg-csub", "csub", [{ v: false, l: "使わない" }, { v: true, l: "使う" }], () => COST.sub, v => COST.sub = v);
+ document.getElementById("ctl-cyears").hidden = COST.pay !== "buy";
+ document.getElementById("ctl-csub").hidden = COST.pay !== "buy";
+ seg("seg-cwage", "cwage", [1100, 1177, 1300, 1500].map(v => ({ v, l: yen(v) })), () => COST.wage, v => COST.wage = v);
+ seg("seg-chours", "chours", [{ v: "peak", l: "ピークの間" }, { v: "shift", l: `${COST_SHIFT_H}時間` }], () => COST.hours, v => COST.hours = v);
+ seg("seg-cpeaks", "cpeaks", [{ v: 1, l: "1日1回" }, { v: 2, l: "1日2回" }], () => COST.peaks, v => COST.peaks = v);
+}
+function renderCost() {
+ const s = S(), arr = arrN();
+ const h = hoursSaved(), perPerson = h * COST.peaks * COST_DAYS * COST.wage;
+ document.getElementById("cost-sub").textContent =
+ `${s.label}・${CROWD[state.crowd]}(${arr}組/時)・${MODE_SHORT[state.mode]}・${state.trays === 1 ? "1卓ずつ" : "最大3卓"}は、左の条件と同じです。` +
+ `ロボットの台数ごとに、店が回る最少のスタッフが何人減るか(人件費)と、人数をそのままにしたときの人手不足・作業の軽さ・サービスの変化を、ロボットの費用と並べます。`;
+ const maxR = Math.max(3, state.robots);
+ const base = minStaff(0);
+ const rows = ROBOTS_ALL.filter(n => n <= maxR).map(n => {
+ const m = minStaff(n), c = robotCost(n);
+ const saved = base && m ? base.st - m.st : null;
+ const labor = saved === null ? null : saved * perPerson;
+ return { n, m, c, saved, labor, net: labor === null ? null : labor - c.month };
+ });
+ const nets = rows.filter(x => x.n > 0 && x.net !== null).map(x => x.net);
+ const bestNet = nets.length ? Math.max(...nets) : null;
+ const head = `| ロボット | 店が回る最少のスタッフ | そのときの入店待ち | ロボットの費用 | 浮く人件費 | 差し引き |
`;
+ const body = rows.map(x => {
+ const cur = x.n === state.robots ? " cur" : "";
+ if (!x.m) return `
| ${x.n}台 | 選べる人数(${s.staff_all[0]}〜${s.staff_all[s.staff_all.length - 1]}人)では店が回りません |
`;
+ const first = COST.pay === "buy" && x.n > 0
+ ? `初めに ${man(x.c.first)}${x.c.subsidy ? `(補助 ${man(x.c.subsidy)})` : ""}` : "";
+ const lab = x.n === 0 ? "基準" : x.saved > 0 ? `${man(x.labor)}${x.saved}人分` : x.saved === 0 ? `0円人数は同じ` : `${man(x.labor)}${-x.saved}人増える`;
+ const net = x.n === 0 ? "—" : man(x.net);
+ return `| ${x.n}台 | ${x.m.st}人 | ${plainMin(x.m.r.entry_wait_mean)} | `
+ + `${x.n === 0 ? "0円" : man(x.c.month)}${first} | ${lab} | `
+ + `${net} |
`;
+ }).join("");
+ document.getElementById("costtable").innerHTML = head + `${body}`;
+
+ // 人数をそのままにしてロボットを入れたとき(左のスタッフ数):人手不足・作業の軽さ・サービス
+ const staff = state.staff;
+ document.getElementById("cost2-h").textContent = `人数をそのまま(スタッフ${staff}人)にしてロボットを入れたとき`;
+ const same = ROBOTS_ALL.filter(n => n <= maxR).map(n => ({ n, r: find(state.store, staff, n, arr, state.mode), c: robotCost(n) }));
+ const r0 = same[0].r;
+ const diff = (a, b) => { const d = a ? b / a - 1 : 0; return Math.abs(d) < 0.005 ? "" : `${d < 0 ? "−" : "+"}${Math.abs(Math.round(d * 100))}%`; };
+ const head2 = `| ロボット | ロボットの費用 | 手がふさがっている割合 | 1人が歩く距離 | 入店待ち | 料理が届くまで |
`;
+ const body2 = same.map(({ n, r, c }) => {
+ if (!r) return "";
+ const cur = n === state.robots ? " cur" : "";
+ return `| ${n}台 | ${n === 0 ? "0円" : man(c.month)} | `
+ + `${plainPct(r.staff_util)} | ${r.walk_km_per_staff.toFixed(2)}km${n ? diff(r0.walk_km_per_staff, r.walk_km_per_staff) : ""} | `
+ + `${seatCell(r)} | ${plainSec(r.dish_wait_mean)} |
`;
+ }).join("");
+ document.getElementById("costtable2").innerHTML = head2 + `${body2}`;
+
+ // 左で選んでいる台数についての一文
+ const n = state.robots, x = rows.find(y => y.n === n);
+ let v;
+ if (!base) v = `ロボットなしでは、選べる人数の範囲で店が回らないので、減らせる人数を比べられません。左の混み具合を下げると比べられます。`;
+ else if (n === 0) v = `左でロボットを1台以上選ぶと、その台数の費用と、浮く人件費をここにまとめます。ロボットなしで店が回る最少のスタッフは${base.st}人です。`;
+ else if (!x || !x.m) v = `ロボット${n}台でも、選べる人数の範囲では店が回りません。`;
+ else {
+ const wait = `入店待ちは ${plainMin(base.r.entry_wait_mean)} → ${plainMin(x.m.r.entry_wait_mean)}`;
+ const cost = COST.pay === "rent" ? `ロボットの費用は月${man(x.c.month)}` : `ロボットの費用は、初めに${man(x.c.first)}${x.c.subsidy ? `(補助金${man(x.c.subsidy)}を引いた額)` : ""}、${COST.years}年で割ると月${man(x.c.month)}`;
+ if (x.saved > 0) {
+ const back = COST.pay === "buy" && x.labor > x.c.run ? `。初めに払う額は、およそ${Math.ceil(x.c.first / (x.labor - x.c.run))}か月で取り戻せる計算です` : "";
+ v = `ロボット${n}台なら、店が回る最少のスタッフは${base.st}人 → ${x.m.st}人です(${wait})。浮く人件費は月${man(x.labor)}、${cost}で、差し引き月${man(x.net)}${back}。`
+ + (x.net < 0 ? `人件費だけでは元が取れないので、待ち時間や料理が届く早さの良さ(上の表)と合わせて判断する形になります。` : "");
+ } else if (x.saved === 0) {
+ const sr = same.find(y => y.n === n), a0 = r0, b0 = sr && sr.r;
+ const walk = b0 ? Math.round((1 - b0.walk_km_per_staff / a0.walk_km_per_staff) * 100) : 0;
+ const gains = b0 ? `スタッフ${staff}人のままなら、手がふさがっている割合は ${plainPct(a0.staff_util)} → ${plainPct(b0.staff_util)}、1人が歩く距離は${walk >= 0 ? `${walk}%減り` : `${-walk}%増え`}、入店待ちは ${plainMin(a0.entry_wait_mean)} → ${plainMin(b0.entry_wait_mean)}、料理が届くまでは ${plainSec(a0.dish_wait_mean)} → ${plainSec(b0.dish_wait_mean)} です。` : "";
+ v = `ロボット${n}台を入れても、店が回る最少のスタッフは${base.st}人のままで、人件費は浮きません。${cost}で、変わるのは人手不足・作業の軽さ・サービスの方です。` + gains;
+ } else {
+ v = `ロボット${n}台では、店が回る最少のスタッフがかえって増えます(${base.st}人 → ${x.m.st}人)。`;
+ }
+ if (n >= 5) v += ROBOT_NOTE;
+ }
+ document.getElementById("cost-verdict").textContent = v;
+
+ // 欄の頭のカード3枚(左で選んでいる台数。0台なら2台で見せる)
+ const kn = state.robots || 2, kx = rows.find(y => y.n === kn), ks = same.find(y => y.n === kn);
+ const of0 = state.robots === 0 ? "(0台を選んでいるので2台で)" : "";
+ let cards;
+ if (!kx) cards = [];
+ else {
+ const c = kx.c;
+ const costH = COST.pay === "rent"
+ ? `1台 月${man(COST.price.rent)}+保守・充電 × ${kn}台`
+ : `初めに${man(c.first)}${c.subsidy ? `(補助金${man(c.subsidy)}を引いた額)` : ""}を${COST.years}年で割り、保守・充電を足した額`;
+ const labV = !base || !kx.m ? "—" : man(Math.max(kx.labor, 0)).replace(/^0万円$/, "0円");
+ const labH = !base ? "ロボットなしで店が回らないので比べられない"
+ : !kx.m ? "選べる人数では店が回らない"
+ : kx.saved > 0 ? `店が回る最少のスタッフ ${base.st}人 → ${kx.m.st}人(${kx.saved}人分)`
+ : `店が回る最少のスタッフは${base.st}人のまま`;
+ const walk = ks && ks.r ? Math.round((1 - ks.r.walk_km_per_staff / r0.walk_km_per_staff) * 100) : null;
+ const netV = kx.net === null ? "—" : man(kx.net);
+ const netH = kx.net === null ? ""
+ : kx.saved > 0 && COST.pay === "buy" && kx.labor > c.run ? `初めに払う額は、およそ${Math.ceil(c.first / (kx.labor - c.run))}か月で取り戻せる`
+ : kx.saved > 0 ? "浮く人件費 − ロボットの費用"
+ : ks && ks.r ? `人数そのままなら、歩く距離 ${walk >= 0 ? "−" : "+"}${Math.abs(walk)}%・入店待ち ${plainMin(r0.entry_wait_mean)} → ${plainMin(ks.r.entry_wait_mean)}` : "";
+ const yen = v => v.replace("万円", "万円").replace(/^0円$/, "0円");
+ cards = [
+ { k: `ロボット${kn}台の費用(月)${of0}`, v: yen(man(c.month)), h: costH },
+ { k: "浮く人件費(月)", v: labV === "—" ? "—" : yen(labV), h: labH },
+ { k: "差し引き(月)", v: netV === "—" ? "—" : yen(netV), h: netH },
+ ];
+ }
+ document.getElementById("cost-kpis").innerHTML = cards.map(x => `${x.k}${x.v}${x.h}
`).join("");
+
+ const a = [
+ ["人件費の差の出し方", `左の条件(店・混み具合・使い方・運び方)のまま、ロボット0〜${maxR}台それぞれで「店が回らない」が出ない最少のスタッフを、上の試算の結果から探す。ロボットなしより減った人数 × 浮く時間 × ピークの回数 × ${COST_DAYS}日 × 時給`],
+ ["浮く時間", `「ピークの間」は、この店のピーク${s.peak_min}分。「4時間」はシフト1回分。いまの設定では、1人あたり月${man(perPerson)}`],
+ ["時給", "社会保険料・交通費などの上乗せは入れていない。2026年度の最低賃金は全国加重平均で1,177円(9月3日に各都道府県の答申が出そろった。10月1日〜12月2日に順に発効)"],
+ ["ロボットの値段の相場", "買う場合は150〜350万円(中央値250万円前後)、レンタル・リースは月3〜12万円(中央値は6万円台)"],
+ ["保守・充電", `リースの月額に保守が含まれず、別に数万円かかる契約がある。保守の金額は仮置きで、込み・1万円・2万円から選ぶ。充電の電気代は1台月${COST_CHARGE.toLocaleString("ja-JP")}円で固定(目安は月1,000〜3,000円)`],
+ ["買う場合のひと月の費用", "(買った値段 − 補助金)÷ 使う月数 + 保守 + 充電。残った価値(売るときの値段)は入れていない"],
+ ["補助金(中小企業省力化投資補助金・カタログ注文型)", `カタログに載った製品を販売事業者と一緒に申請する。補助率は1/2以下。上限は従業員5人以下 500万円・6〜20人 750万円・21人以上 1,000万円(大幅な賃上げをすると上限が上がる。2026年3月19日の改定後)。この試算では買う場合にだけ1/2を引き、上限は750万円で計算する。随時受付で2027年3月末ごろまでの予定(予算がなくなると早く終わることがある)。申請には GビズIDプライム(取得に2〜3週間)が要る。配膳ロボットは対象の製品として扱われている。リースでの導入が対象になるかは、公募要領で確かめる`],
+ ["そのほかの支援", "業務改善助成金(厚生労働省):店の中で一番低い時給を50円以上上げ、設備投資などをすると費用の3/4〜4/5が出る。令和8年度の申請は、店のある都道府県の最低賃金が発効する前日まで。配膳ロボットが対象になるかは労働局で確かめる"],
+ ];
+ const src = [
+ ["配膳ロボットの導入費用の相場(お名前.com ビジネス)", "https://www.onamae.com/business/article/306061/"],
+ ["配膳ロボットのリース・保守・充電の費用(創業手帳)", "https://sogyotecho.jp/inshokuten-haizenrobot/"],
+ ["2026年度の地域別最低賃金(労働政策研究・研修機構)", "https://www.jil.go.jp/kokunai/blt/backnumber/2026/10/special_03.html"],
+ ["省力化投資補助金 カタログ注文型(補助金ポータル)", "https://hojyokin-portal.jp/columns/shoryokuka-catalog"],
+ ["省力化補助金の介護業での活用(厚生労働省・令和8年4月30日)", "https://www.mhlw.go.jp/content/001696868.pdf"],
+ ["令和8年度 業務改善助成金のご案内(厚生労働省)", "https://www.mhlw.go.jp/content/11200000/001693381.pdf"],
+ ];
+ document.getElementById("cost-assume").innerHTML = a.map(([k, t]) => `${k}:${t}`).join("")
+ + `出典:${src.map(([t, u]) => `${t}`).join("、")}`;
+}
+
@@ -673,0 +932 @@ buildControls();
+buildCost();
@@ -674,0 +934,8 @@ render();
+document.getElementById("cost-jump").addEventListener("click", e => {
+ const t = document.getElementById("cost");
+ let f = null; try { f = window.frameElement; } catch (_) {}
+ if (!f) return;
+ e.preventDefault();
+ const pw = window.parent;
+ pw.scrollTo({ top: f.getBoundingClientRect().top + pw.scrollY + t.getBoundingClientRect().top - 96, behavior: "smooth" });
+});