お店の売上分析ツール「Mise」

【更新履歴】

 ・2026/2/20 バージョン1.0公開。
 ・2026/2/20 バージョン1.1公開。


画像
メイン画面

・ダウンロードされる方はこちら。↓

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MISE v2 — 小さなお店の販売分析</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Kaisei+Opti:wght@400;700&family=IBM+Plex+Sans+JP:wght@300;400;500;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
  --bg0:#12100e; --bg1:#1c1916; --bg2:#252119; --bg3:#2f2b22;
  --border:#3a342a; --border2:#4a4238;
  --text0:#f0e8d8; --text1:#c8b99a; --text2:#8a7a66; --text3:#5a4e40;
  --amber:#e8a230; --amber-l:rgba(232,162,48,0.14); --amber-d:#b87d1a;
  --green:#5cb85a; --green-l:rgba(92,184,90,0.12);
  --red:#e05050;   --red-l:rgba(224,80,80,0.12);
  --blue:#4a9fd4;  --blue-l:rgba(74,159,212,0.12);
  --purple:#a07cd4;--purple-l:rgba(160,124,212,0.12);
  --teal:#40b8a0;  --teal-l:rgba(64,184,160,0.12);
  --r:6px; --sh:0 4px 24px rgba(0,0,0,0.4);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
html{scroll-behavior:smooth;}
body{background:var(--bg0);color:var(--text0);font-family:'IBM Plex Sans JP',sans-serif;font-size:13px;line-height:1.6;min-height:100vh;overflow-x:hidden;}
::-webkit-scrollbar{width:5px;height:5px;}
::-webkit-scrollbar-track{background:var(--bg1);}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px;}

/* ── Header ── */
header{position:sticky;top:0;z-index:300;background:var(--bg1);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:16px;padding:0 20px;height:52px;}
.logo{font-family:'Kaisei Opti',serif;font-size:20px;letter-spacing:.12em;color:var(--amber);}
.logo-sub{font-size:10px;color:var(--text2);letter-spacing:.2em;text-transform:uppercase;border-left:1px solid var(--border);padding-left:14px;white-space:nowrap;}
.hdr-r{margin-left:auto;display:flex;align-items:center;gap:10px;}
.hkpi{display:flex;flex-direction:column;align-items:flex-end;padding:3px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);}
.hkpi-l{font-size:9px;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;}
.hkpi-v{font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:600;color:var(--amber);}

/* ── Layout ── */
.layout{display:grid;grid-template-columns:272px 1fr;min-height:calc(100vh - 52px);}
.side{background:var(--bg1);border-right:1px solid var(--border);overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px;}
.ss{border-bottom:1px solid var(--border);padding-bottom:12px;}
.ss:last-child{border-bottom:none;}
.sl{font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--text3);margin-bottom:7px;font-family:'JetBrains Mono',monospace;}

/* forms */
.f{display:flex;flex-direction:column;gap:3px;margin-bottom:7px;}
.f label{font-size:11px;color:var(--text2);}
input[type=text],input[type=number],input[type=date],select,textarea{background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:6px 9px;font-family:inherit;font-size:12px;color:var(--text0);outline:none;width:100%;transition:border-color .15s;}
input:focus,select:focus,textarea:focus{border-color:var(--amber);}
select option{background:var(--bg2);}
textarea{resize:vertical;min-height:50px;font-size:11px;}

.btn{display:flex;align-items:center;justify-content:center;gap:5px;padding:7px 12px;border-radius:var(--r);font-family:inherit;font-size:12px;font-weight:500;cursor:pointer;border:none;width:100%;transition:all .15s;white-space:nowrap;}
.btn-amber{background:var(--amber);color:#12100e;}
.btn-amber:hover{background:#f0b040;box-shadow:0 2px 10px rgba(232,162,48,.3);}
.btn-outline{background:transparent;color:var(--text1);border:1px solid var(--border);}
.btn-outline:hover{border-color:var(--border2);background:var(--bg2);}
.btn-green{background:var(--green);color:#12100e;}
.btn-green:hover{background:#6cd468;}
.btn-sm{padding:5px 9px;font-size:11px;width:auto;}
.btn:disabled{opacity:.4;cursor:not-allowed;}

.drop-zone{border:2px dashed var(--border2);border-radius:var(--r);padding:14px 10px;text-align:center;cursor:pointer;color:var(--text2);font-size:12px;background:var(--bg2);transition:all .2s;}
.drop-zone:hover,.drop-zone.drag{border-color:var(--amber);color:var(--amber);background:var(--amber-l);}
.drop-zone input{display:none;}

/* log */
#log{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text2);background:var(--bg0);border:1px solid var(--border);border-radius:var(--r);padding:5px 7px;max-height:68px;overflow-y:auto;}
#log div{margin-bottom:1px;}
#log .ok{color:var(--green);}#log .err{color:var(--red);}#log .info{color:var(--blue);}#log .warn{color:var(--amber);}

/* campaign list */
.cp-list{display:flex;flex-direction:column;gap:3px;max-height:120px;overflow-y:auto;}
.cp-item{display:flex;align-items:center;gap:5px;padding:4px 7px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);font-size:11px;}
.cp-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;}
.cp-name{flex:1;color:var(--text1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cp-dates{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text2);}
.cp-del{cursor:pointer;color:var(--text3);padding:0 2px;}.cp-del:hover{color:var(--red);}

/* ── Main ── */
.main{display:flex;flex-direction:column;overflow-y:auto;}

/* Banner */
.banner{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border-bottom:1px solid var(--border);padding:14px 22px;display:grid;grid-template-columns:auto 1fr repeat(3,auto);gap:14px;align-items:center;}
.banner.hidden{display:none;}
.s-ring{width:46px;height:46px;border-radius:50%;border:2px solid var(--border2);background:var(--bg2);display:flex;align-items:center;justify-content:center;font-size:22px;transition:all .4s;}
.s-ring.good{border-color:var(--green);background:var(--green-l);box-shadow:0 0 12px rgba(92,184,90,.2);}
.s-ring.warn{border-color:var(--amber);background:var(--amber-l);box-shadow:0 0 12px rgba(232,162,48,.2);}
.s-ring.bad {border-color:var(--red);  background:var(--red-l);  box-shadow:0 0 12px rgba(224,80,80,.2);}
.s-lbl{font-size:9px;color:var(--text2);letter-spacing:.1em;text-align:center;margin-top:2px;}
.b-msg{}
.b-hl{font-family:'Kaisei Opti',serif;font-size:15px;color:var(--text0);line-height:1.4;}
.b-dt{font-size:11px;color:var(--text2);margin-top:2px;}
.bkpi{display:flex;flex-direction:column;align-items:flex-end;padding:6px 12px;background:var(--bg0);border:1px solid var(--border);border-radius:var(--r);min-width:100px;}
.bkpi-l{font-size:9px;color:var(--text3);letter-spacing:.1em;text-transform:uppercase;}
.bkpi-v{font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:600;color:var(--text0);}
.bkpi-v.up{color:var(--green);}.bkpi-v.down{color:var(--red);}
.bkpi-s{font-size:10px;color:var(--text3);}

/* Tabs */
.tab-bar{display:flex;overflow-x:auto;background:var(--bg1);border-bottom:1px solid var(--border);padding:0 18px;position:sticky;top:52px;z-index:200;}
.tab-bar::-webkit-scrollbar{height:2px;}
.tab{padding:11px 14px;font-size:12px;cursor:pointer;color:var(--text2);border-bottom:2px solid transparent;white-space:nowrap;transition:all .15s;font-weight:500;display:flex;align-items:center;gap:5px;}
.tab:hover{color:var(--text1);}
.tab.active{color:var(--amber);border-bottom-color:var(--amber);}
.tbadge{background:var(--red);color:#fff;font-size:9px;padding:1px 5px;border-radius:8px;font-family:'JetBrains Mono',monospace;}

/* Content */
.tab-content{display:none;padding:18px 22px;}
.tab-content.active{display:block;}

/* Card */
.card{background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);padding:15px;margin-bottom:14px;}
.card-hd{display:flex;align-items:baseline;gap:8px;margin-bottom:3px;}
.card-t{font-size:13px;font-weight:700;color:var(--text0);}
.card-sub{font-size:11px;color:var(--text2);margin-bottom:11px;}
.card canvas{max-height:240px;}
.card canvas.tall{max-height:300px;}
.card canvas.short{max-height:160px;}

/* Grids */
.g2{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;}

/* Advice */
.adv-list{display:flex;flex-direction:column;gap:8px;}
.adv{display:flex;gap:9px;padding:11px 13px;border-radius:var(--r);border-left:4px solid var(--border);background:var(--bg2);animation:fu .3s ease;}
@keyframes fu{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.adv.good{border-left-color:var(--green);background:var(--green-l);}
.adv.warn{border-left-color:var(--amber);background:var(--amber-l);}
.adv.bad {border-left-color:var(--red);  background:var(--red-l);}
.adv.info{border-left-color:var(--blue); background:var(--blue-l);}
.adv.purple{border-left-color:var(--purple);background:var(--purple-l);}
.adv.teal  {border-left-color:var(--teal);  background:var(--teal-l);}
.adv-icon{font-size:17px;flex-shrink:0;}
.adv-t{font-weight:700;font-size:13px;color:var(--text0);margin-bottom:2px;}
.adv-d{font-size:12px;color:var(--text1);line-height:1.65;}
/* アクションボックス */
.action-box{margin-top:6px;padding:7px 10px;background:rgba(0,0,0,.2);border-radius:4px;font-size:11px;color:var(--amber);border-left:2px solid var(--amber);}

/* Heatmap */
.hm-wrap{overflow-x:auto;}
.heatmap{display:grid;grid-template-columns:48px repeat(24,1fr);gap:2px;min-width:680px;}
.hm-cell{height:26px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-family:'JetBrains Mono',monospace;cursor:default;transition:transform .1s;position:relative;}
.hm-cell:hover{transform:scale(1.12);z-index:2;}
.hm-lbl{background:transparent;font-size:10px;color:var(--text2);justify-content:flex-start;}
.hm-hdr{background:transparent;font-size:8px;color:var(--text3);height:18px;}

/* Rank table */
.rtbl{width:100%;border-collapse:collapse;font-size:12px;}
.rtbl th{font-size:9px;letter-spacing:.12em;color:var(--text3);text-align:left;padding:7px 9px;border-bottom:1px solid var(--border);font-family:'JetBrains Mono',monospace;text-transform:uppercase;}
.rtbl td{padding:8px 9px;border-bottom:1px solid var(--border);vertical-align:middle;}
.rtbl tr:hover td{background:var(--bg2);}
.rbar-w{height:5px;background:var(--bg3);border-radius:3px;min-width:70px;}
.rbar{height:5px;border-radius:3px;transition:width .6s;}
.badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:9px;font-size:10px;font-weight:600;white-space:nowrap;font-family:'JetBrains Mono',monospace;}
.bg{background:var(--green-l);color:var(--green);}
.br{background:var(--red-l);color:var(--red);}
.ba{background:var(--amber-l);color:var(--amber);}
.bb{background:var(--blue-l);color:var(--blue);}
.bp{background:var(--purple-l);color:var(--purple);}
.bt{background:var(--teal-l);color:var(--teal);}

/* Campaign effect */
.ce-wrap{display:grid;grid-template-columns:1fr auto 1fr;gap:0;align-items:center;margin-bottom:14px;}
.ce-box{padding:14px;text-align:center;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2);}
.ce-arr{display:flex;flex-direction:column;align-items:center;padding:0 10px;}
.ce-chg{font-family:'JetBrains Mono',monospace;font-weight:600;font-size:18px;}
.ce-lbl{font-size:10px;color:var(--text2);margin-bottom:5px;}
.ce-val{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:600;color:var(--text0);}

/* Purchase */
.pw-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:3px;margin-top:10px;}
.pw-hdr{text-align:center;font-size:10px;color:var(--text2);padding:3px;font-family:'JetBrains Mono',monospace;}
.pw-cell{padding:7px 3px;text-align:center;border-radius:4px;font-size:10px;cursor:default;transition:transform .1s;}
.pw-cell:hover{transform:scale(1.06);}
.risk-bw{height:7px;background:var(--bg3);border-radius:4px;overflow:hidden;}
.risk-b{height:100%;border-radius:4px;transition:width .8s;}

/* Quick entry */
.qe-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px;}
.qe-btn{padding:14px 6px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);text-align:center;cursor:pointer;font-size:12px;color:var(--text1);transition:all .15s;}
.qe-btn:hover{border-color:var(--amber);color:var(--amber);}
.qe-entry{display:flex;flex-direction:column;gap:6px;margin-top:8px;}

/* Format modal */
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:500;align-items:center;justify-content:center;}
.modal-bg.show{display:flex;}
.modal{background:var(--bg1);border:1px solid var(--border);border-radius:8px;padding:22px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;}
.modal h3{font-family:'Kaisei Opti',serif;font-size:16px;color:var(--amber);margin-bottom:14px;}
.modal code{font-family:'JetBrains Mono',monospace;font-size:11px;background:var(--bg0);padding:8px 12px;display:block;border-radius:4px;margin:6px 0;color:var(--text1);white-space:pre;}
.modal .close{float:right;cursor:pointer;color:var(--text2);font-size:18px;}

/* Empty */
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:240px;color:var(--text3);gap:9px;text-align:center;}
.empty-icon{font-size:40px;opacity:.35;}

/* Loading */
#loading{display:none;position:fixed;inset:0;background:rgba(18,16,14,.85);backdrop-filter:blur(4px);z-index:999;align-items:center;justify-content:center;flex-direction:column;gap:12px;}
#loading.show{display:flex;}
.lr{width:36px;height:36px;border:3px solid var(--border);border-top-color:var(--amber);border-radius:50%;animation:sp .75s linear infinite;}
@keyframes sp{to{transform:rotate(360deg)}}

/* Filter row */
.frow{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-bottom:12px;padding:9px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);}
.frow label{font-size:11px;color:var(--text2);white-space:nowrap;}
.frow select,.frow input[type=text]{flex:0 0 auto;width:auto;}

/* Col-map */
.colmap-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
.colmap-row label{font-size:11px;color:var(--text1);width:60px;flex-shrink:0;}
.colmap-row select{flex:1;}
.colmap-preview{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--teal);margin-top:4px;padding:5px 8px;background:var(--bg0);border-radius:4px;}

@media(max-width:900px){
  .layout{grid-template-columns:1fr;}
  .side{border-right:none;border-bottom:1px solid var(--border);}
  .g2,.g3{grid-template-columns:1fr;}
  .banner{grid-template-columns:auto 1fr;}
}
</style>
</head>
<body>
<div id="loading"><div class="lr"></div><div id="ldmsg" style="font-size:12px;color:var(--text2)">分析中...</div></div>

<!-- Format Modal -->
<div class="modal-bg" id="fmt-modal">
  <div class="modal">
    <span class="close" onclick="document.getElementById('fmt-modal').classList.remove('show')">✕</span>
    <h3>📋 CSVフォーマットガイド</h3>
    <p style="font-size:12px;color:var(--text1);margin-bottom:12px">以下のいずれかの形式に対応しています。ヘッダー行の有無は自動判定します。</p>
    <p style="font-size:11px;color:var(--text2);margin-bottom:4px">▶ 最小構成(日付と金額だけ)</p>
    <code>日付,金額
2024-01-15,1500
2024-01-15,980</code>
    <p style="font-size:11px;color:var(--text2);margin-bottom:4px;margin-top:10px">▶ 標準(商品名付き)</p>
    <code>日時,商品名,数量,金額
2024-01-15 12:30,コーヒー,2,760
2024-01-15 13:05,ランチ,1,850</code>
    <p style="font-size:11px;color:var(--text2);margin-bottom:4px;margin-top:10px">▶ 詳細(カテゴリ付き)</p>
    <code>日時,商品名,カテゴリ,数量,金額
2024-01-15 12:30,コーヒー,ドリンク,2,760</code>
    <p style="font-size:11px;color:var(--text2);margin-bottom:4px;margin-top:10px">▶ 対応POSアプリ</p>
    <p style="font-size:12px;color:var(--text1)"><b>Square:</b> 「レポート」→「売上サマリー」→「エクスポート」<br>
    <b>Airレジ:</b> 「データ管理」→「売上データ」→「CSV出力」<br>
    <b>Excel:</b> 「名前を付けて保存」→「CSV (コンマ区切り)」</p>
    <p style="font-size:11px;color:var(--text3);margin-top:12px">※ 列の順番は問いません。列名から自動で判定します。判定できない場合は手動で設定できます。</p>
  </div>
</div>

<!-- ── Header ── -->
<header>
  <div class="logo">MISE</div>
  <div class="logo-sub">小さなお店の販売分析 v2</div>
  <div class="hdr-r">
    <span id="store-disp" style="font-size:11px;color:var(--text3)">データ未読み込み</span>
    <div id="hdr-kpis" style="display:none;display:flex;gap:8px">
      <div class="hkpi"><div class="hkpi-l">総売上</div><div class="hkpi-v" id="hkv-s">─</div></div>
      <div class="hkpi"><div class="hkpi-l">取引数</div><div class="hkpi-v" id="hkv-t">─</div></div>
      <div class="hkpi"><div class="hkpi-l">客単価</div><div class="hkpi-v" id="hkv-a">─</div></div>
    </div>
  </div>
</header>

<div class="layout">
<!-- ══ Side ══ -->
<div class="side">

  <div class="ss">
    <div class="sl">店舗設定</div>
    <div class="f"><label>店舗名</label><input type="text" id="storeName" placeholder="例: さくら食堂"></div>
    <div class="f"><label>業種</label>
      <select id="storeType">
        <option value="food">飲食・カフェ</option>
        <option value="retail">小売・雑貨</option>
        <option value="grocery">食品・青果</option>
        <option value="other">その他</option>
      </select>
    </div>
  </div>

  <div class="ss">
    <div class="sl">CSVを読み込む</div>
    <div class="drop-zone" id="dropZone"
         onclick="document.getElementById('csvFile').click()"
         ondragover="onDragOver(event)" ondrop="onDrop(event)">
      <input type="file" id="csvFile" accept=".csv,.txt" onchange="loadCSV(this)">
      <div style="font-size:20px;opacity:.55;margin-bottom:5px">📂</div>
      <div style="font-weight:500">CSVをドロップ / クリックで選択</div>
      <div style="font-size:10px;margin-top:5px;color:var(--text3);line-height:1.7">Square・Airレジ・Excel対応<br>日時 / 商品名 / 数量 / 金額</div>
    </div>
    <div style="display:flex;gap:5px;margin-top:7px">
      <button class="btn btn-outline btn-sm" onclick="document.getElementById('fmt-modal').classList.add('show')" style="flex:1">📋 書式ガイド</button>
      <button class="btn btn-outline btn-sm" onclick="loadSample()" style="flex:1">▶ サンプル</button>
    </div>
  </div>

  <!-- 列マッピング(自動検出失敗時に表示) -->
  <div class="ss" id="colmap-sec" style="display:none">
    <div class="sl">列の対応を設定</div>
    <div id="colmap-fields"></div>
    <div class="colmap-preview" id="colmap-preview"></div>
    <button class="btn btn-amber" onclick="applyColMap()" style="margin-top:7px">適用して分析</button>
  </div>

  <div class="ss">
    <div class="sl">手入力(紙帳簿から)</div>
    <div style="display:flex;gap:5px;margin-bottom:6px">
      <button class="btn btn-outline btn-sm" onclick="showQuickEntry()" style="flex:1">+ 1件入力</button>
      <button class="btn btn-outline btn-sm" onclick="showBulkEntry()" style="flex:1">📋 まとめ入力</button>
    </div>
    <div id="quick-entry-area" style="display:none">
      <div class="f"><label>日時</label><input type="text" id="qe-dt" placeholder="2024-03-15 12:30"></div>
      <div class="f"><label>商品名</label><input type="text" id="qe-prod" placeholder="コーヒー" list="prod-datalist"></div>
      <datalist id="prod-datalist"></datalist>
      <div class="f"><label>金額</label><input type="number" id="qe-amt" placeholder="480"></div>
      <div class="f"><label>数量</label><input type="number" id="qe-qty" placeholder="1" value="1"></div>
      <div style="display:flex;gap:5px">
        <button class="btn btn-amber btn-sm" onclick="addQuickEntry()" style="flex:1">追加</button>
        <button class="btn btn-outline btn-sm" onclick="document.getElementById('quick-entry-area').style.display='none'" style="flex:1">閉じる</button>
      </div>
    </div>
    <div id="bulk-entry-area" style="display:none">
      <div class="f">
        <label>「日時,商品名,数量,金額」を1行ずつ</label>
        <textarea id="bulk-text" placeholder="2024-03-15 12:30,コーヒー,1,480&#10;2024-03-15 13:00,ランチ,1,850"></textarea>
      </div>
      <div style="display:flex;gap:5px">
        <button class="btn btn-amber btn-sm" onclick="applyBulkEntry()" style="flex:1">読み込む</button>
        <button class="btn btn-outline btn-sm" onclick="document.getElementById('bulk-entry-area').style.display='none'" style="flex:1">閉じる</button>
      </div>
    </div>
  </div>

  <div class="ss">
    <div class="sl">分析期間</div>
    <div class="f"><label>開始日</label><input type="date" id="fStart"></div>
    <div class="f"><label>終了日</label><input type="date" id="fEnd"></div>
    <button class="btn btn-outline" onclick="applyFilter()" style="margin-top:2px">絞り込む</button>
  </div>

  <div class="ss">
    <div class="sl">キャンペーン登録</div>
    <div class="f"><label>名称</label><input type="text" id="cpName" placeholder="例: 春のセール"></div>
    <div class="f"><label>開始日</label><input type="date" id="cpStart"></div>
    <div class="f"><label>終了日</label><input type="date" id="cpEnd"></div>
    <button class="btn btn-amber" onclick="addCampaign()">登録</button>
    <div class="cp-list" id="cp-list" style="margin-top:7px"></div>
  </div>

  <div class="ss">
    <div class="sl">ログ</div>
    <div id="log"></div>
  </div>

</div><!-- /side -->

<!-- ══ Main ══ -->
<div class="main">

<!-- Banner -->
<div class="banner hidden" id="banner">
  <div style="display:flex;flex-direction:column;align-items:center">
    <div class="s-ring" id="s-ring">─</div>
    <div class="s-lbl" id="s-lbl">─</div>
  </div>
  <div class="b-msg">
    <div class="b-hl" id="b-hl">データを読み込んでください</div>
    <div class="b-dt" id="b-dt"></div>
  </div>
  <div class="bkpi" id="bk1" style="display:none"><div class="bkpi-l">先週比</div><div class="bkpi-v" id="bk-wow">─</div><div class="bkpi-s">売上</div></div>
  <div class="bkpi" id="bk2" style="display:none"><div class="bkpi-l">売れ筋</div><div class="bkpi-v" style="font-size:12px" id="bk-top">─</div><div class="bkpi-s">数量1位</div></div>
  <div class="bkpi" id="bk3" style="display:none"><div class="bkpi-l">ピーク</div><div class="bkpi-v" id="bk-peak">─</div><div class="bkpi-s">最売上時間</div></div>
</div>

<!-- Tabs -->
<div class="tab-bar" id="main-tab-bar">
  <div class="tab active" onclick="switchTab('overview',this)">📊 売上の流れ</div>
  <div class="tab" onclick="switchTab('heatmap',this)">🕐 時間帯ヒートマップ</div>
  <div class="tab" onclick="switchTab('products',this)">📦 商品ランキング</div>
  <div class="tab" onclick="switchTab('campaign',this)">🎯 キャンペーン効果</div>
  <div class="tab" onclick="switchTab('purchase',this)">🛒 仕入れ提案</div>
  <div class="tab" onclick="switchTab('advice',this)">💡 アドバイス<span class="tbadge" id="adv-badge" style="display:none">0</span></div>
</div>

<!-- ── Overview ── -->
<div class="tab-content active" id="tab-overview">
  <div class="empty" id="emp-overview"><div class="empty-icon">📊</div><div style="color:var(--text2)">CSVを読み込むか「サンプル」を押してください</div></div>
  <div id="ov-content" style="display:none">
    <div class="frow">
      <label>集計</label>
      <select id="ov-unit" onchange="buildOverview()"><option value="day">日次</option><option value="week">週次</option><option value="month" selected>月次</option></select>
      <label style="margin-left:6px">商品</label>
      <select id="ov-prod" onchange="buildOverview()" style="max-width:130px"><option value="all">すべて</option></select>
    </div>
    <div class="card">
      <div class="card-t">売上推移</div>
      <div class="card-sub" id="ov-sub">─</div>
      <canvas id="ch-ov" class="tall"></canvas>
    </div>
    <div class="g2">
      <div class="card"><div class="card-t">曜日別 平均売上</div><div class="card-sub">何曜日が稼ぎ頭か</div><canvas id="ch-dow"></canvas></div>
      <div class="card"><div class="card-t">月別 平均売上</div><div class="card-sub">季節の波を把握する</div><canvas id="ch-mon"></canvas></div>
    </div>
  </div>
</div>

<!-- ── Heatmap ── -->
<div class="tab-content" id="tab-heatmap">
  <div class="empty" id="emp-heatmap"><div class="empty-icon">🕐</div><div style="color:var(--text2)">時間付きデータが必要です(例: 2024-01-15 12:30)</div></div>
  <div id="hm-content" style="display:none">
    <div class="frow">
      <label>指標</label>
      <select id="hm-metric" onchange="buildHeatmap()"><option value="amount">売上金額</option><option value="qty">販売数量</option><option value="tx">取引件数</option></select>
      <label style="margin-left:6px">商品</label>
      <select id="hm-prod" onchange="buildHeatmap()" style="max-width:130px"><option value="all">すべて</option></select>
    </div>
    <div class="card">
      <div class="card-t">曜日 × 時間帯 ヒートマップ</div>
      <div class="card-sub">🔥 色が濃い=よく売れる。仕入れタイミング・人員配置の目安に。</div>
      <div class="hm-wrap" id="hm-grid"></div>
    </div>
    <div class="g2">
      <div class="card"><div class="card-t">時間帯別 売上合計</div><div class="card-sub">ピーク時間はここ</div><canvas id="ch-hour"></canvas></div>
      <div class="card"><div class="card-t">時間帯別 売れ筋TOP3</div><div class="card-sub">時間帯ごとに何が売れているか</div><div id="hr-top3" style="margin-top:6px"></div></div>
    </div>
  </div>
</div>

<!-- ── Products ── -->
<div class="tab-content" id="tab-products">
  <div class="empty" id="emp-products"><div class="empty-icon">📦</div><div style="color:var(--text2)">商品名付きのデータが必要です</div></div>
  <div id="pr-content" style="display:none">
    <div class="frow">
      <label>並び順</label>
      <select id="pr-sort" onchange="buildProducts()"><option value="amount">売上金額</option><option value="qty">販売数量</option><option value="freq">購入頻度</option></select>
      <label style="margin-left:6px">カテゴリ</label>
      <select id="pr-cat" onchange="buildProducts()" style="max-width:120px"><option value="all">すべて</option></select>
      <label style="margin-left:6px">件数</label>
      <select id="pr-lim" onchange="buildProducts()"><option value="10">10件</option><option value="20" selected>20件</option><option value="50">50件</option></select>
    </div>
    <div class="card">
      <div class="card-t">商品ランキング</div>
      <div class="card-sub" id="pr-sub">─</div>
      <div style="overflow-x:auto">
        <table class="rtbl">
          <thead><tr><th style="width:32px">#</th><th>商品名</th><th>カテゴリ</th><th>売上金額</th><th style="min-width:90px">構成比</th><th>数量</th><th>単価</th><th>曜日傾向</th><th>判定</th></tr></thead>
          <tbody id="pr-tbody"></tbody>
        </table>
      </div>
    </div>
    <div class="g2">
      <div class="card"><div class="card-t">カテゴリ構成</div><div class="card-sub">どのカテゴリが主力か</div><canvas id="ch-cat"></canvas></div>
      <div class="card"><div class="card-t">上位5商品 月別推移</div><div class="card-sub">主力商品の季節変動</div><canvas id="ch-toptrend"></canvas></div>
    </div>
  </div>
</div>

<!-- ── Campaign ── -->
<div class="tab-content" id="tab-campaign">
  <div class="empty" id="emp-campaign"><div class="empty-icon">🎯</div><div style="color:var(--text2)">左パネルでキャンペーンを登録してください</div><div style="font-size:11px;color:var(--text3)">名称・開始日・終了日を入れるだけで自動比較</div></div>
  <div id="cp-content" style="display:none">
    <div id="cp-cards"></div>
    <div class="card">
      <div class="card-t">キャンペーン期間 売上比較チャート</div>
      <div class="card-sub">全期間の推移にキャンペーン期間を重ねて表示</div>
      <canvas id="ch-cp" class="tall"></canvas>
    </div>
  </div>
</div>

<!-- ── Purchase ── -->
<div class="tab-content" id="tab-purchase">
  <div class="empty" id="emp-purchase"><div class="empty-icon">🛒</div><div style="color:var(--text2)">データを読み込むと仕入れ提案が表示されます</div></div>
  <div id="pu-content" style="display:none">
    <div class="g2">
      <div class="card">
        <div class="card-t">🗓️ 今週の仕入れ量目安</div>
        <div class="card-sub">過去データから算出した曜日別の推奨仕入れ量</div>
        <div class="pw-grid" id="pu-dow-grid"></div>
        <div id="pu-dow-adv" style="margin-top:10px;display:flex;flex-direction:column;gap:6px"></div>
      </div>
      <div class="card">
        <div class="card-t">🕐 品切れリスクの高い時間帯</div>
        <div class="card-sub">需要が急増する時間帯。事前に補充を</div>
        <div id="stockout-list" style="margin-top:6px;display:flex;flex-direction:column;gap:7px"></div>
      </div>
    </div>
    <div class="card">
      <div class="card-t">📦 商品別 仕入れ推奨量(1日あたり)</div>
      <div class="card-sub">売れ行きの速さと売上貢献度から算出。変動が大きい商品は多めに確保を。</div>
      <div style="overflow-x:auto;margin-top:8px">
        <table class="rtbl">
          <thead><tr><th>商品名</th><th>平均日販</th><th>ピーク曜日(×倍率)</th><th>推奨仕入れ量</th><th>変動</th><th>優先度</th><th>コメント</th></tr></thead>
          <tbody id="pu-tbody"></tbody>
        </table>
      </div>
    </div>
    <div class="card">
      <div class="card-t">📅 季節別 仕入れカレンダー</div>
      <div class="card-sub">月ごとの売上傾向から、いつ仕入れを増やすべきかがわかります</div>
      <canvas id="ch-pu-season"></canvas>
      <div id="pu-season-adv" style="margin-top:10px;display:flex;flex-direction:column;gap:6px"></div>
    </div>
  </div>
</div>

<!-- ── Advice ── -->
<div class="tab-content" id="tab-advice">
  <div class="empty" id="emp-advice"><div class="empty-icon">💡</div><div style="color:var(--text2)">データを読み込むと自動でアドバイスが表示されます</div></div>
  <div id="adv-content" style="display:none">
    <div class="card" style="margin-bottom:14px">
      <div class="card-t">📋 分析サマリー</div>
      <div class="card-sub" id="adv-summary"></div>
    </div>
    <div class="adv-list" id="adv-list"></div>
  </div>
</div>

</div><!-- /main -->
</div><!-- /layout -->
<script>
/* ══════════════════════════════════════════════
   MISE v2 — 分析エンジン
   対応: Square/Airレジ/Excel/手入力 1050種商品
   ══════════════════════════════════════════════ */
let allRows=[], filteredRows=[], campaigns=[], colMap={}, csvHeaders=[];
const CP_COLORS=['#e8a230','#5cb85a','#4a9fd4','#a07cd4','#e05050','#40b8a0'];
const DOW=['日','月','火','水','木','金','土'];
const MON=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
let charts={};

// ── utils ──────────────────────────────────────
const $=id=>document.getElementById(id);
const log=(msg,t='info')=>{const el=$('log');const d=document.createElement('div');d.className=t;d.textContent='> '+msg;el.appendChild(d);el.scrollTop=el.scrollHeight;};
const fY=n=>n==null||isNaN(n)?'─':'¥'+Math.round(n).toLocaleString();
const fN=(n,d=0)=>n==null||isNaN(n)?'─':n.toFixed(d).replace(/\B(?=(\d{3})+(?!\d))/g,',');
const fP=n=>(n>=0?'+':'')+n.toFixed(1)+'%';
const avg=a=>a.length?a.reduce((s,v)=>s+v,0)/a.length:0;
const sum=a=>a.reduce((s,v)=>s+v,0);
const std=a=>{const m=avg(a);return Math.sqrt(a.reduce((s,v)=>s+(v-m)**2,0)/(a.length||1));};
const dc=id=>{if(charts[id]){charts[id].destroy();delete charts[id];}};

// Chart.js defaults
const CD={
  responsive:true,maintainAspectRatio:true,animation:{duration:450},
  plugins:{
    legend:{labels:{color:'#8a7a66',font:{family:"'IBM Plex Sans JP'",size:10},boxWidth:11}},
    tooltip:{backgroundColor:'rgba(18,16,14,.95)',borderColor:'#3a342a',borderWidth:1,
      titleColor:'#f0e8d8',bodyColor:'#c8b99a',padding:9,
      titleFont:{family:"'JetBrains Mono'",size:10},bodyFont:{family:"'JetBrains Mono'",size:10}}
  },
  scales:{
    x:{ticks:{color:'#5a4e40',font:{family:"'JetBrains Mono'",size:9},maxTicksLimit:14},grid:{color:'rgba(58,52,42,.4)'},border:{color:'#3a342a'}},
    y:{ticks:{color:'#5a4e40',font:{family:"'JetBrains Mono'",size:9}},grid:{color:'rgba(58,52,42,.4)'},border:{color:'#3a342a'}}
  }
};
function co(e={}){return JSON.parse(JSON.stringify({...CD,...e}));}

// ── CSV ────────────────────────────────────────
function onDragOver(e){e.preventDefault();$('dropZone').classList.add('drag');}
function onDrop(e){e.preventDefault();$('dropZone').classList.remove('drag');if(e.dataTransfer.files[0])parseFile(e.dataTransfer.files[0]);}
function loadCSV(i){if(i.files[0])parseFile(i.files[0]);}
function parseFile(f){
  const r=new FileReader();
  r.onload=e=>{try{parseText(e.target.result,f.name.replace(/\.csv$/i,''));}catch(err){log('読込エラー: '+err.message,'err');}};
  r.readAsText(f,'UTF-8');
}

// ── Square / Airレジ / 汎用 自動認識 ────────────
function detectPOSFormat(headers){
  // Square: "Date","Time","Category","Item","Qty","Price"
  if(headers.some(h=>/^(Date|Time)$/i.test(h))&&headers.some(h=>/^(Item|Description)$/i.test(h))){
    return 'square';
  }
  // Airレジ: 売上日時, 商品名, 金額(税込)
  if(headers.some(h=>/売上日時/.test(h))&&headers.some(h=>/商品名/.test(h))){
    return 'airregi';
  }
  return 'generic';
}

function autoMap(headers){
  const m={datetime:-1,product:-1,category:-1,qty:-1,amount:-1};
  const fmt=detectPOSFormat(headers);
  if(fmt==='square'){
    // Square形式: DateとTimeを結合する特別処理フラグ
    m._squareDate=headers.findIndex(h=>/^Date$/i.test(h));
    m._squareTime=headers.findIndex(h=>/^Time$/i.test(h));
    m.datetime=m._squareDate; // Dateを基準に
    m.product =headers.findIndex(h=>/^(Item|Description|Name)$/i.test(h));
    m.category=headers.findIndex(h=>/^Category$/i.test(h));
    m.qty     =headers.findIndex(h=>/^(Qty|Quantity|Units)$/i.test(h));
    m.amount  =headers.findIndex(h=>/^(Gross Sales|Price|Amount|Net Sales|Total|売上)$/i.test(h));
    if(m.amount<0) m.amount=headers.findIndex(h=>/price|sales|amount|total/i.test(h));
    m._squareFmt=true;
    log('Square形式を検出','ok');
    return m;
  }
  if(fmt==='airregi'){
    m.datetime=headers.findIndex(h=>/売上日時/.test(h));
    m.product =headers.findIndex(h=>/商品名/.test(h));
    m.category=headers.findIndex(h=>/カテゴリ|分類/.test(h));
    m.qty     =headers.findIndex(h=>/数量|個数/.test(h));
    m.amount  =headers.findIndex(h=>/金額|売上/.test(h));
    log('Airレジ形式を検出','ok');
    return m;
  }
  // 汎用マッピング
  headers.forEach((h,i)=>{
    const l=h.toLowerCase();
    if(m.datetime<0&&/日時|日付|time|date|timestamp|売上日/i.test(h)) m.datetime=i;
    if(m.product <0&&/商品|品名|item|product|メニュー|menu|name|品目/i.test(h)) m.product=i;
    if(m.category<0&&/カテゴリ|category|分類|種別|genre|部門/i.test(h)) m.category=i;
    if(m.qty     <0&&/数量|個数|qty|quantity|点数|枚数|杯数|units/i.test(h)) m.qty=i;
    if(m.amount  <0&&/金額|売上|amount|price|revenue|合計|小計|税込|total|gross|net/i.test(h)) m.amount=i;
  });
  return m;
}

function parseText(text,name=''){
  const lines=text.trim().split(/\r?\n/).filter(l=>l.trim());
  if(lines.length<2) throw new Error('データが少なすぎます');
  const first=splitLine(lines[0]);
  const isHeader=first.some(c=>/[^\d\-\/: .,¥$]/.test(c));
  csvHeaders=isHeader?first:first.map((_,i)=>`列${i+1}`);
  const startRow=isHeader?1:0;
  colMap=autoMap(csvHeaders);
  log(`列検出: datetime=${colMap.datetime} product=${colMap.product} amount=${colMap.amount}`,'info');

  if(colMap.datetime<0&&colMap.amount<0){
    showColMapUI(csvHeaders,lines.slice(startRow,startRow+3).map(splitLine));
    return;
  }
  const raw=[];
  for(let i=startRow;i<lines.length;i++){
    const cells=splitLine(lines[i]);
    // Square: DateとTimeを結合
    if(colMap._squareFmt&&colMap._squareTime>=0&&cells[colMap._squareDate]&&cells[colMap._squareTime]){
      cells[colMap.datetime]=cells[colMap._squareDate]+' '+cells[colMap._squareTime];
    }
    const row=parseRow(cells,colMap);
    if(row) raw.push(row);
  }
  if(!raw.length) throw new Error('有効な行がありません。書式ガイドを確認してください。');
  raw.sort((a,b)=>(a.datetime||'').localeCompare(b.datetime||''));
  allRows=raw; filteredRows=[...raw];
  const sn=$('storeName').value||name||'お店のデータ';
  $('storeName').value=sn;
  const dates=raw.filter(r=>r.date).map(r=>r.date).sort();
  if(dates.length){$('fStart').value=dates[0];$('fEnd').value=dates[dates.length-1];}
  updateProdSelects(); updateCatSelect();
  updateProdDatalist();
  log(`読込完了: ${raw.length}件 (${dates[0]}〜${dates[dates.length-1]})`,'ok');
  runAll();
}

function splitLine(line){
  const res=[];let cur='',inQ=false;
  for(let i=0;i<line.length;i++){
    const c=line[i];
    if(c==='"'){inQ=!inQ;}
    else if(c===','&&!inQ){res.push(cur.trim());cur='';}
    else cur+=c;
  }
  res.push(cur.trim());
  return res.map(c=>c.replace(/^["']|["']$/g,'').trim());
}

function parseRow(cells,m){
  try{
    const dtStr=m.datetime>=0?cells[m.datetime]:'';
    const dt=parseDT(dtStr);
    const amtRaw=m.amount>=0?cells[m.amount]:null;
    if(amtRaw==null) return null;
    const amt=parseFloat(String(amtRaw).replace(/[¥,$\s,]/g,'').replace(/[^0-9.\-]/g,''));
    if(isNaN(amt)||amt<0) return null;
    const qtyRaw=m.qty>=0?cells[m.qty]:null;
    const qty=qtyRaw!=null?parseFloat(qtyRaw):1;
    const prod=m.product>=0?(cells[m.product]||'不明'):'不明';
    const cat =m.category>=0?(cells[m.category]||'未分類'):'未分類';
    return{datetime:dt.full,date:dt.date,hour:dt.hour,dow:dt.dow,month:dt.month,year:dt.year,product:prod,category:cat,qty:isNaN(qty)?1:qty,amount:amt};
  }catch{return null;}
}

function parseDT(s){
  if(!s)return{full:'',date:'',hour:null,dow:null,month:null,year:null};
  s=s.replace(/\//g,'-').replace(/\s+/g,' ').trim();
  const pats=[
    /^(\d{4})-(\d{1,2})-(\d{1,2})[ T](\d{1,2}):(\d{2})/,
    /^(\d{4})-(\d{1,2})-(\d{1,2})/,
    /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/,
    /^(\d{1,2})-(\d{1,2})-(\d{4})\s+(\d{1,2}):(\d{2})/,
  ];
  for(const p of pats){
    const m=s.match(p);
    if(m){
      let y,mo,d,h=null;
      if(p===pats[3]){mo=parseInt(m[1]);d=parseInt(m[2]);y=parseInt(m[3]);h=parseInt(m[4]);}
      else{y=parseInt(m[1]);mo=parseInt(m[2]);d=parseInt(m[3]);if(m[4]!=null)h=parseInt(m[4]);}
      const dt2=new Date(y,mo-1,d,h||0);
      return{full:s,date:`${y}-${String(mo).padStart(2,'0')}-${String(d).padStart(2,'0')}`,
        hour:h,dow:dt2.getDay(),month:mo,year:y};
    }
  }
  return{full:s,date:'',hour:null,dow:null,month:null,year:null};
}

// ── 列マッピングUI ──────────────────────────────
function showColMapUI(headers,samples){
  $('colmap-sec').style.display='block';
  const roles=[{k:'datetime',l:'日時'},{k:'product',l:'商品名'},{k:'category',l:'カテゴリ'},{k:'qty',l:'数量'},{k:'amount',l:'金額'}];
  $('colmap-fields').innerHTML=roles.map(r=>`
    <div class="colmap-row">
      <label>${r.l}</label>
      <select id="cm-${r.k}">
        <option value="-1">なし</option>
        ${headers.map((h,i)=>`<option value="${i}" ${colMap[r.k]==i?'selected':''}>${h}</option>`).join('')}
      </select>
    </div>`).join('');
  // プレビュー
  $('colmap-preview').textContent=samples[0]?'1行目: '+samples[0].join(' | '):'';
}
function applyColMap(){
  ['datetime','product','category','qty','amount'].forEach(k=>{
    const el=$('cm-'+k);if(el)colMap[k]=parseInt(el.value);
  });
  $('colmap-sec').style.display='none';
  if(allRows.length)runAll();
}

// ── Filter ─────────────────────────────────────
function applyFilter(){
  const s=$('fStart').value,e=$('fEnd').value;
  filteredRows=allRows.filter(r=>{if(s&&r.date<s)return false;if(e&&r.date>e)return false;return true;});
  log(`絞り込み: ${filteredRows.length}件`,'ok');
  runAll(false);
}

// ── 商品セレクト更新 ────────────────────────────
function updateProdSelects(){
  const prods=[...new Set(filteredRows.map(r=>r.product))].sort();
  ['ov-prod','hm-prod'].forEach(id=>{
    const sel=$(id);if(!sel)return;
    const cur=sel.value;
    sel.innerHTML='<option value="all">すべて</option>'+prods.map(p=>`<option value="${p}">${p}</option>`).join('');
    sel.value=prods.includes(cur)?cur:'all';
  });
}
function updateCatSelect(){
  const cats=[...new Set(filteredRows.map(r=>r.category))].sort();
  const sel=$('pr-cat');if(!sel)return;
  const cur=sel.value;
  sel.innerHTML='<option value="all">すべて</option>'+cats.map(c=>`<option value="${c}">${c}</option>`).join('');
  sel.value=cats.includes(cur)?cur:'all';
}
function updateProdDatalist(){
  const prods=[...new Set(allRows.map(r=>r.product))].sort();
  $('prod-datalist').innerHTML=prods.map(p=>`<option value="${p}">`).join('');
}

// ── 手入力 ─────────────────────────────────────
function showQuickEntry(){$('quick-entry-area').style.display='block';$('bulk-entry-area').style.display='none';}
function showBulkEntry(){$('bulk-entry-area').style.display='block';$('quick-entry-area').style.display='none';}
function addQuickEntry(){
  const dt=$('qe-dt').value.trim()||new Date().toISOString().slice(0,16).replace('T',' ');
  const prod=$('qe-prod').value.trim()||'不明';
  const amt=parseFloat($('qe-amt').value);
  const qty=parseFloat($('qe-qty').value)||1;
  if(isNaN(amt)){log('金額を入力してください','warn');return;}
  const parsed=parseDT(dt);
  const row={datetime:parsed.full||dt,date:parsed.date||dt.slice(0,10),
    hour:parsed.hour,dow:parsed.dow,month:parsed.month,year:parsed.year,
    product:prod,category:'手入力',qty,amount:amt};
  allRows.push(row);
  allRows.sort((a,b)=>(a.datetime||'').localeCompare(b.datetime||''));
  filteredRows=[...allRows];
  updateProdSelects();updateCatSelect();updateProdDatalist();
  $('qe-dt').value='';$('qe-amt').value='';$('qe-qty').value='1';
  log(`追加: ${prod} ${fY(amt)}`,'ok');
  runAll(false);
}
function applyBulkEntry(){
  const text=$('bulk-text').value.trim();
  if(!text){log('データを入力してください','warn');return;}
  try{parseText(text,'手入力データ');}catch(e){log(e.message,'err');}
  $('bulk-entry-area').style.display='none';
}

// ── Campaign ────────────────────────────────────
function addCampaign(){
  const name=$('cpName').value.trim(),s=$('cpStart').value,e=$('cpEnd').value;
  if(!name||!s||!e){log('名称・開始日・終了日を入力してください','warn');return;}
  if(s>e){log('開始日 > 終了日です','warn');return;}
  campaigns.push({name,start:s,end:e,color:CP_COLORS[campaigns.length%CP_COLORS.length]});
  $('cpName').value='';renderCPList();
  if(allRows.length)buildCampaign();
  log(`登録: ${name}`,'ok');
}
function renderCPList(){
  const el=$('cp-list');
  if(!campaigns.length){el.innerHTML='';return;}
  el.innerHTML=campaigns.map((c,i)=>`
    <div class="cp-item">
      <div class="cp-dot" style="background:${c.color}"></div>
      <div class="cp-name">${c.name}</div>
      <div class="cp-dates">${c.start.slice(5)}〜${c.end.slice(5)}</div>
      <div class="cp-del" onclick="delCP(${i})">✕</div>
    </div>`).join('');
}
function delCP(i){campaigns.splice(i,1);renderCPList();if(allRows.length)buildCampaign();}

// ── Header / Banner ─────────────────────────────
function updateHeader(){
  const sn=$('storeName').value||'お店';
  $('store-disp').textContent=sn;
  $('hdr-kpis').style.display='flex';
  const totalAmt=sum(filteredRows.map(r=>r.amount));
  const txSet=new Set(filteredRows.map(r=>r.datetime||r.date));
  const txCnt=txSet.size||filteredRows.length;
  $('hkv-s').textContent=fY(totalAmt);
  $('hkv-t').textContent=txCnt.toLocaleString()+'';
  $('hkv-a').textContent=fY(totalAmt/txCnt);
}
function updateBanner(){
  $('banner').classList.remove('hidden');
  const rows=filteredRows;
  const dates=[...new Set(rows.map(r=>r.date))].sort();
  const last7=dates.slice(-7),prev7=dates.slice(-14,-7);
  const wSum=ds=>sum(rows.filter(r=>ds.includes(r.date)).map(r=>r.amount));
  const thisW=wSum(last7),prevW=wSum(prev7);
  const wow=prevW>0?(thisW-prevW)/prevW*100:0;
  let state,emoji,hl,dt2;
  if(wow>8){state='good';emoji='📈';hl='売上は好調です';dt2=`先週比 ${fP(wow)} の上昇。この調子を維持しましょう。`;}
  else if(wow>-5){state='warn';emoji='➡️';hl='売上は横ばい推移';dt2=`先週比 ${fP(wow)}。大きな変化はありません。`;}
  else{state='bad';emoji='📉';hl='売上がやや下落しています';dt2=`先週比 ${fP(wow)}。仕入れや品揃えを見直すタイミングかもしれません。`;}
  $('s-ring').className='s-ring '+state;$('s-ring').textContent=emoji;
  $('s-lbl').textContent={good:'好調',warn:'横ばい',bad:'注意'}[state];
  $('b-hl').textContent=hl;$('b-dt').textContent=dt2;
  const wowEl=$('bk-wow');wowEl.textContent=fP(wow);wowEl.className='bkpi-v '+(wow>=0?'up':'down');
  const byProd={};rows.forEach(r=>{byProd[r.product]=(byProd[r.product]||0)+r.qty;});
  const topP=Object.entries(byProd).sort((a,b)=>b[1]-a[1])[0];
  $('bk-top').textContent=topP?topP[0]:'';
  const hAmt={};rows.filter(r=>r.hour!=null).forEach(r=>{hAmt[r.hour]=(hAmt[r.hour]||0)+r.amount;});
  const peakH=Object.entries(hAmt).sort((a,b)=>b[1]-a[1])[0];
  $('bk-peak').textContent=peakH?peakH[0]+'時台':'';
  ['bk1','bk2','bk3'].forEach(id=>$(id).style.display='flex');
}

// ── Sample ─────────────────────────────────────
function loadSample(){
  allRows=genSample();filteredRows=[...allRows];
  $('storeName').value='さくら食堂(サンプル)';
  const dates=allRows.map(r=>r.date).sort();
  $('fStart').value=dates[0];$('fEnd').value=dates[dates.length-1];
  updateProdSelects();updateCatSelect();updateProdDatalist();
  campaigns=[
    {name:'春のランチ割引',start:'2024-03-01',end:'2024-03-31',color:'#e8a230'},
    {name:'夏フェア',start:'2024-07-15',end:'2024-08-15',color:'#5cb85a'},
  ];
  renderCPList();
  log('サンプルデータ読込: '+allRows.length+'件(飲食店1年分)','ok');
  runAll();
}
function genSample(){
  const prods=[
    {n:'日替わりランチ',c:'ランチ',bq:8,p:850,pk:[11,12,13]},
    {n:'コーヒー',c:'ドリンク',bq:15,p:380,pk:[8,9,10,14,15]},
    {n:'カフェラテ',c:'ドリンク',bq:10,p:450,pk:[9,10,11,15]},
    {n:'アイスコーヒー',c:'ドリンク',bq:12,p:400,pk:[12,13,14,15]},
    {n:'ケーキセット',c:'スイーツ',bq:6,p:750,pk:[14,15,16]},
    {n:'モーニング',c:'モーニング',bq:10,p:680,pk:[7,8,9]},
    {n:'パスタ',c:'ランチ',bq:5,p:950,pk:[12,13]},
    {n:'サンドウィッチ',c:'ランチ',bq:4,p:620,pk:[11,12,13]},
    {n:'プリン',c:'スイーツ',bq:7,p:320,pk:[14,15,16,17]},
    {n:'紅茶',c:'ドリンク',bq:5,p:350,pk:[14,15,16]},
  ];
  const sea=[.85,.82,.95,1.0,1.05,1.0,1.15,1.18,1.0,.98,1.05,1.25];
  const df=[.7,.9,.95,.95,1.0,1.3,1.2];
  const rows=[];
  const s=new Date('2024-01-01'),e=new Date('2024-12-31');
  for(let d=new Date(s);d<=e;d.setDate(d.getDate()+1)){
    const dow=d.getDay(),mo=d.getMonth();
    const ds=d.toISOString().slice(0,10);
    const cp1=ds>='2024-03-01'&&ds<='2024-03-31';
    const cp2=ds>='2024-07-15'&&ds<='2024-08-15';
    const cb=cp1?1.25:cp2?1.18:1.0;
    prods.forEach(p=>{
      const hrs=dow===0||dow===6?[8,9,10,11,12,13,14,15,16,17,18]:[7,8,9,10,11,12,13,14,15,16,17,18];
      hrs.forEach(h=>{
        const ip=p.pk.includes(h);
        const cnt=Math.max(0,Math.round(p.bq*(ip?1.6:.3)*sea[mo]*df[dow]*cb*(.7+Math.random()*.6)));
        for(let i=0;i<cnt;i++)rows.push({
          datetime:`${ds} ${String(h).padStart(2,'0')}:${String(Math.floor(Math.random()*60)).padStart(2,'0')}`,
          date:ds,hour:h,dow,month:mo+1,year:2024,
          product:p.n,category:p.c,qty:1,amount:p.p*(.95+Math.random()*.1)
        });
      });
    });
  }
  return rows.sort((a,b)=>a.datetime.localeCompare(b.datetime));
}

// ── Tab switch ─────────────────────────────────
function switchTab(name,el){
  $('main-tab-bar').querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
  document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));
  el.classList.add('active');
  $('tab-'+name).classList.add('active');
}

// ── runAll ─────────────────────────────────────
function runAll(full=true){
  $('loading').classList.add('show');
  $('ldmsg').textContent='分析中...';
  setTimeout(()=>{
    try{
      updateHeader();updateBanner();
      if(full){buildOverview();buildHeatmap();buildProducts();buildCampaign();buildPurchase();buildAdvice();}
      showAll();
    }catch(e){log('エラー: '+e.message,'err');console.error(e);}
    $('loading').classList.remove('show');
  },60);
}
function setC(id,show){$('emp-'+id).style.display=show?'none':'flex';$(id.replace('-','') + '-content')?$(id.split('-')[0]+'-content').style.display=show?'block':'none':null;}
function showAll(){
  const hasT=filteredRows.some(r=>r.hour!=null);
  const hasP=filteredRows.some(r=>r.product&&r.product!=='不明');
  [['emp-overview','ov-content',true],['emp-heatmap','hm-content',hasT],
   ['emp-products','pr-content',hasP],['emp-campaign','cp-content',campaigns.length>0],
   ['emp-purchase','pu-content',true],['emp-advice','adv-content',true]
  ].forEach(([eid,cid,show])=>{
    $(eid).style.display=show?'none':'flex';
    $(cid).style.display=show?'block':'none';
  });
}

// ══════════════════════════════════════════════
// ─── 売上の流れ ─────────────────────────────
function buildOverview(){
  const unit=$('ov-unit').value,prod=$('ov-prod').value;
  let rows=filteredRows;
  if(prod!=='all')rows=rows.filter(r=>r.product===prod);
  const bp={};
  rows.forEach(r=>{
    let k;
    if(unit==='day')k=r.date;
    else if(unit==='week'){const d=new Date(r.date);d.setDate(d.getDate()-d.getDay());k=d.toISOString().slice(0,10);}
    else k=r.date.slice(0,7);
    if(!bp[k])bp[k]=0;bp[k]+=r.amount;
  });
  const labels=Object.keys(bp).sort(),amts=labels.map(k=>bp[k]);
  const maN=Math.min(Math.max(3,Math.floor(labels.length/6)),8);
  const ma=amts.map((_,i)=>i<maN-1?null:avg(amts.slice(i-maN+1,i+1)));
  const rN=Math.min(5,Math.floor(labels.length/3));
  const rA=avg(amts.slice(-rN)),oA=avg(amts.slice(-rN*2,-rN))||rA;
  const tr=(rA-oA)/oA*100;
  $('ov-sub').textContent=`${labels[0]}〜${labels[labels.length-1]} 直近傾向: ${Math.abs(tr)<2?'横ばい':tr>0?`上昇(${fP(tr)})`:` 下落(${fP(tr)})`}`;
  dc('ov');
  const ctx=$('ch-ov').getContext('2d');
  const g=ctx.createLinearGradient(0,0,0,260);g.addColorStop(0,'rgba(232,162,48,.22)');g.addColorStop(1,'rgba(232,162,48,.01)');
  charts['ov']=new Chart(ctx,{type:'line',data:{labels,datasets:[
    {label:'売上',data:amts,borderColor:'#e8a230',borderWidth:2,backgroundColor:g,fill:true,tension:.3,pointRadius:amts.length>60?0:3,pointBackgroundColor:'#e8a230'},
    {label:`${maN}期移動平均`,data:ma,borderColor:'#c8b99a',borderWidth:1.5,borderDash:[5,4],pointRadius:0,fill:false,tension:.3}
  ]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.dataset.label+': '+fY(c.raw)}}}}});
  // 曜日
  const dA=new Array(7).fill(0),dC=new Array(7).fill(0);
  rows.forEach(r=>{if(r.dow!=null){dA[r.dow]+=r.amount;dC[r.dow]++;}});
  const da=dA.map((a,i)=>dC[i]?a/dC[i]:0),mxD=Math.max(...da);
  dc('dow');charts['dow']=new Chart($('ch-dow').getContext('2d'),{type:'bar',data:{labels:DOW,datasets:[{label:'平均売上',data:da,backgroundColor:da.map(v=>v>=mxD*.9?'rgba(232,162,48,.85)':v>=mxD*.7?'rgba(232,162,48,.55)':'rgba(90,78,64,.5)'),borderRadius:4,borderWidth:0}]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>fY(c.raw)}}}}});
  // 月別
  const mA=new Array(12).fill(0),mC=new Array(12).fill(0);
  rows.forEach(r=>{if(r.month){mA[r.month-1]+=r.amount;mC[r.month-1]++;}});
  const ma2=mA.map((a,i)=>mC[i]?a/mC[i]:0),totM=avg(ma2.filter(v=>v>0));
  dc('mon');charts['mon']=new Chart($('ch-mon').getContext('2d'),{type:'bar',data:{labels:MON,datasets:[{label:'月平均',data:ma2,backgroundColor:ma2.map(v=>v>totM*1.1?'rgba(92,184,90,.75)':v<totM*.9?'rgba(224,80,80,.6)':'rgba(74,159,212,.6)'),borderRadius:4,borderWidth:0}]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>fY(c.raw)}}}}});
}

// ─── ヒートマップ ────────────────────────────
function buildHeatmap(){
  const metric=$('hm-metric').value,prod=$('hm-prod').value;
  let rows=filteredRows.filter(r=>r.hour!=null);
  if(prod!=='all')rows=rows.filter(r=>r.product===prod);
  if(!rows.length)return;
  const mat=Array.from({length:7},()=>new Array(24).fill(0));
  const cnt=Array.from({length:7},()=>new Array(24).fill(0));
  rows.forEach(r=>{
    if(r.dow==null||r.hour==null)return;
    if(metric==='amount')mat[r.dow][r.hour]+=r.amount;
    else if(metric==='qty')mat[r.dow][r.hour]+=r.qty;
    else cnt[r.dow][r.hour]++;
  });
  const data=metric==='tx'?cnt:mat;
  const all=data.flat().filter(v=>v>0),mx=Math.max(...all,1);
  let html='<div class="heatmap"><div class="hm-cell hm-hdr"></div>';
  for(let h=0;h<24;h++)html+=`<div class="hm-cell hm-hdr">${h}</div>`;
  for(let d=0;d<7;d++){
    html+=`<div class="hm-cell hm-lbl">${DOW[d]}</div>`;
    for(let h=0;h<24;h++){
      const v=data[d][h],r2=mx>0?v/mx:0;
      const bg=r2<.05?'var(--bg3)':`rgba(${Math.round(210+v/mx*20)},${Math.round(100+v/mx*60)},${Math.round(20)},${.15+r2*.7})`;
      const lbl=r2>.35?(metric==='amount'?`${Math.round(v/1000)}k`:Math.round(v)):'';
      html+=`<div class="hm-cell" style="background:${bg};color:${r2>.6?'#12100e':'#8a7a66'}" title="${DOW[d]} ${h}時: ${metric==='amount'?fY(v):fN(v)}">${lbl}</div>`;
    }
  }
  html+='</div>';
  $('hm-grid').innerHTML=html;
  // 時間帯棒グラフ
  const hAmt=new Array(24).fill(0);
  rows.forEach(r=>{hAmt[r.hour]+=r.amount;});
  const pkH=hAmt.indexOf(Math.max(...hAmt));
  dc('hour');charts['hour']=new Chart($('ch-hour').getContext('2d'),{type:'bar',data:{labels:[...Array(24).keys()].map(h=>h+'時'),datasets:[{label:'売上',data:hAmt,backgroundColor:hAmt.map((_,i)=>i===pkH?'rgba(232,162,48,.9)':'rgba(90,78,64,.5)'),borderRadius:3,borderWidth:0}]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>fY(c.raw)}}}}});
  // 時間帯別TOP3
  const hp={};rows.forEach(r=>{if(!hp[r.hour])hp[r.hour]={};hp[r.hour][r.product]=(hp[r.hour][r.product]||0)+r.amount;});
  const topH=[...Array(24).keys()].sort((a,b)=>hAmt[b]-hAmt[a]).slice(0,5);
  $('hr-top3').innerHTML=topH.map(h=>{
    const ps=Object.entries(hp[h]||{}).sort((a,b)=>b[1]-a[1]).slice(0,3);
    return `<div style="margin-bottom:8px"><div style="font-size:10px;color:var(--amber);font-family:'JetBrains Mono',monospace;margin-bottom:2px">▶ ${h}時台</div>${ps.map((p,i)=>`<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text1);padding:2px 0"><span>${i+1}. ${p[0]}</span><span style="color:var(--text2)">${fY(p[1])}</span></div>`).join('')}</div>`;
  }).join('');
}

// ─── 商品ランキング ──────────────────────────
function buildProducts(){
  const sort=$('pr-sort').value,cat=$('pr-cat').value,lim=parseInt($('pr-lim').value);
  let rows=filteredRows;
  if(cat!=='all')rows=rows.filter(r=>r.category===cat);
  const bp={};
  rows.forEach(r=>{
    if(!bp[r.product])bp[r.product]={amt:0,qty:0,tx:0,cat:r.category,dates:new Set(),dows:new Array(7).fill(0),dowc:new Array(7).fill(0)};
    bp[r.product].amt+=r.amount;bp[r.product].qty+=r.qty;bp[r.product].tx++;
    bp[r.product].dates.add(r.date);
    if(r.dow!=null){bp[r.product].dows[r.dow]+=r.amount;bp[r.product].dowc[r.dow]++;}
  });
  const total=sum(Object.values(bp).map(v=>v.amt));
  let prods=Object.entries(bp).map(([n,v])=>({
    n,cat:v.cat,amt:v.amt,qty:v.qty,tx:v.tx,freq:v.dates.size,
    avgP:v.qty>0?v.amt/v.qty:0,
    peakDow:v.dows.indexOf(Math.max(...v.dows)),
  }));
  if(sort==='amount')prods.sort((a,b)=>b.amt-a.amt);
  else if(sort==='qty')prods.sort((a,b)=>b.qty-a.qty);
  else prods.sort((a,b)=>b.freq-a.freq);
  const top=prods.slice(0,lim);
  $('pr-sub').textContent=`${prods.length}商品 / 合計 ${fY(total)}`;
  const CC=['#e8a230','#5cb85a','#4a9fd4','#a07cd4','#e05050','#40b8a0','#e88a30','#30b8a0'];
  $('pr-tbody').innerHTML=top.map((p,i)=>{
    const pct=total>0?p.amt/total*100:0;
    const tc=i<3?'var(--amber)':i<10?'var(--green)':'var(--text2)';
    const badge=i===0?`<span class="badge ba">🥇</span>`:i===1?`<span class="badge bb">🥈</span>`:i===2?`<span class="badge bg">🥉</span>`:'';
    return `<tr>
      <td><span style="font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:600;color:${tc}">${i+1}</span></td>
      <td style="font-weight:500">${p.n} ${badge}</td>
      <td><span class="badge bp">${p.cat}</span></td>
      <td style="font-family:'JetBrains Mono',monospace;font-weight:600">${fY(p.amt)}</td>
      <td><div style="display:flex;align-items:center;gap:6px"><div class="rbar-w" style="flex:1"><div class="rbar" style="width:${pct/Math.max(...top.map(x=>x.amt/total*100))*100}%;background:var(--amber)"></div></div><span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text2);width:32px;text-align:right">${fN(pct,1)}%</span></div></td>
      <td style="font-family:'JetBrains Mono',monospace">${fN(p.qty)}</td>
      <td style="font-family:'JetBrains Mono',monospace">${fY(p.avgP)}</td>
      <td style="font-size:11px"><span class="badge ba">${DOW[p.peakDow]}曜</span></td>
      <td>${i<Math.ceil(lim*.3)?'<span class="badge ba">主力</span>':i<Math.ceil(lim*.6)?'<span class="badge bb">安定</span>':'<span class="badge bg">補助</span>'}</td>
    </tr>`;
  }).join('');
  // カテゴリ円グラフ
  const byCat={};rows.forEach(r=>{byCat[r.category]=(byCat[r.category]||0)+r.amount;});
  const cl=Object.keys(byCat),cv=cl.map(k=>byCat[k]);
  dc('cat');charts['cat']=new Chart($('ch-cat').getContext('2d'),{type:'doughnut',data:{labels:cl,datasets:[{data:cv,backgroundColor:CC.slice(0,cl.length),borderColor:'var(--bg1)',borderWidth:2}]},options:{responsive:true,maintainAspectRatio:true,plugins:{legend:{...CD.plugins.legend,position:'right'},tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>`${c.label}: ${fY(c.raw)} (${fN(c.raw/sum(cv)*100,1)}%)`}}}}});
  // 上位5月別
  const top5=prods.slice(0,5);
  const months=[...new Set(rows.map(r=>r.date.slice(0,7)))].sort();
  dc('toptrend');charts['toptrend']=new Chart($('ch-toptrend').getContext('2d'),{type:'line',data:{labels:months,datasets:top5.map((p,i)=>({label:p.n,data:months.map(mo=>sum(rows.filter(r=>r.product===p.n&&r.date.startsWith(mo)).map(r=>r.amount))),borderColor:CC[i],borderWidth:1.5,pointRadius:2,fill:false,tension:.3}))},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.dataset.label+': '+fY(c.raw)}}}}});
}

// ─── キャンペーン効果 ─────────────────────────
function buildCampaign(){
  if(!campaigns.length){$('emp-campaign').style.display='flex';$('cp-content').style.display='none';return;}
  $('emp-campaign').style.display='none';$('cp-content').style.display='block';
  const cards=$('cp-cards');cards.innerHTML='';
  campaigns.forEach((cp)=>{
    const cpR=filteredRows.filter(r=>r.date>=cp.start&&r.date<=cp.end);
    if(!cpR.length)return;
    const d1=new Date(cp.start),d2=new Date(cp.end);
    const days=Math.round((d2-d1)/86400000)+1;
    const preE=new Date(d1);preE.setDate(preE.getDate()-1);
    const preS=new Date(preE);preS.setDate(preS.getDate()-days+1);
    const preR=filteredRows.filter(r=>r.date>=preS.toISOString().slice(0,10)&&r.date<=preE.toISOString().slice(0,10));
    const postS=new Date(d2);postS.setDate(postS.getDate()+1);
    const postE2=new Date(postS);postE2.setDate(postE2.getDate()+days-1);
    const postR=filteredRows.filter(r=>r.date>=postS.toISOString().slice(0,10)&&r.date<=postE2.toISOString().slice(0,10));
    const cpA=sum(cpR.map(r=>r.amount)),preA=sum(preR.map(r=>r.amount));
    const postA=sum(postR.map(r=>r.amount));
    const cpQ=sum(cpR.map(r=>r.qty));
    const vs=preA>0?(cpA-preA)/preA*100:0;
    const vsPost=postR.length&&postA>0?(cpA-postA)/postA*100:null;
    // 商品別インパクト
    const bpCp={},bpPre={};
    cpR.forEach(r=>{bpCp[r.product]=(bpCp[r.product]||0)+r.amount;});
    preR.forEach(r=>{bpPre[r.product]=(bpPre[r.product]||0)+r.amount;});
    const lifts=Object.keys(bpCp).map(p=>({n:p,lift:bpPre[p]>0?(bpCp[p]-bpPre[p])/bpPre[p]*100:100})).sort((a,b)=>b.lift-a.lift).slice(0,3);
    const ok=vs>=0;
    // 次回推奨アクション
    const nextAction=ok
      ? `✅ 効果あり!次回は「${lifts[0]?.n||'主力商品'}」の在庫を${Math.ceil(cpQ/days*1.3)}個/日以上確保し、${cp.end.slice(0,7)}以降も同様のキャンペーンを検討してください。`
      : `⚠️ 効果が薄い結果でした。割引率を5〜10%上げる、または対象商品を「${lifts[0]?.n||'売れ筋'}」に絞り込むと効果が出やすくなります。`;
    const el=document.createElement('div');el.className='card';
    el.innerHTML=`
      <div class="card-hd">
        <div class="card-t">🎯 ${cp.name}</div>
        <span class="badge ${ok?'bg':'br'}">${ok?'効果あり':'効果薄'}</span>
        <span style="font-size:11px;color:var(--text2)">${cp.start}〜${cp.end}(${days}日間)</span>
      </div>
      <div class="ce-wrap">
        <div class="ce-box"><div class="ce-lbl">キャンペーン前(同期間)</div><div class="ce-val">${fY(preA)}</div></div>
        <div class="ce-arr"><div style="font-size:20px">→</div><div class="ce-chg" style="color:${ok?'var(--green)':'var(--red)'}">${fP(vs)}</div></div>
        <div class="ce-box" style="border-color:${cp.color}"><div class="ce-lbl">キャンペーン中</div><div class="ce-val" style="color:${cp.color}">${fY(cpA)}</div></div>
      </div>
      ${vsPost!=null?`<div style="font-size:11px;color:var(--text2);margin-bottom:10px">キャンペーン後との比較: <span style="color:${vsPost>=0?'var(--green)':'var(--red)'}">${fP(vsPost)}</span>(反動確認)</div>`:''}
      ${lifts.length?`<div style="margin-bottom:10px"><div style="font-size:11px;color:var(--text2);margin-bottom:5px">▶ 最も伸びた商品</div>${lifts.map((p,i)=>`<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--border);font-size:12px"><span>${i+1}. ${p.n}</span><span class="badge ${p.lift>=0?'bg':'br'}">${fP(p.lift)}</span></div>`).join('')}</div>`:''}
      <div class="action-box">${nextAction}</div>
    `;
    cards.appendChild(el);
  });
  // 比較チャート
  const months=[...new Set(filteredRows.map(r=>r.date.slice(0,7)))].sort();
  const mAmt=months.map(m=>sum(filteredRows.filter(r=>r.date.startsWith(m)).map(r=>r.amount)));
  dc('cp');
  const ctx=$('ch-cp').getContext('2d');
  const g=ctx.createLinearGradient(0,0,0,280);g.addColorStop(0,'rgba(232,162,48,.2)');g.addColorStop(1,'rgba(232,162,48,.01)');
  charts['cp']=new Chart(ctx,{type:'line',data:{labels:months,datasets:[
    {label:'月次売上',data:mAmt,borderColor:'#e8a230',borderWidth:2,backgroundColor:g,fill:true,tension:.3,pointRadius:4,pointBackgroundColor:'#e8a230'},
    ...campaigns.map(cp=>({
      label:cp.name+'(期間)',
      data:months.map(m=>{const inCp=m>=cp.start.slice(0,7)&&m<=cp.end.slice(0,7);return inCp?sum(filteredRows.filter(r=>r.date.startsWith(m)&&r.date>=cp.start&&r.date<=cp.end).map(r=>r.amount)):null;}),
      borderColor:cp.color,borderWidth:3,backgroundColor:cp.color+'33',fill:true,tension:.3,pointRadius:5,pointBackgroundColor:cp.color,spanGaps:false
    }))
  ]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.raw!=null?c.dataset.label+': '+fY(c.raw):''}}}}});
}

// ─── 仕入れ提案(具体的な数字) ──────────────
function buildPurchase(){
  const rows=filteredRows;
  const dA=new Array(7).fill(0),dC=new Array(7).fill(0);
  rows.forEach(r=>{if(r.dow!=null){dA[r.dow]+=r.amount;dC[r.dow]++;}});
  const da=dA.map((a,i)=>dC[i]?a/dC[i]:0),mxD=Math.max(...da,1);
  // 曜日グリッド(仕入れ前日表示)
  $('pu-dow-grid').innerHTML=
    '<div class="pw-hdr" style="text-align:left">前日→</div>'+DOW.map((d,i)=>`<div class="pw-hdr">${d}→<br><span style="color:var(--amber)">${DOW[(i+1)%7]}</span>前</div>`).join('')+
    '<div class="pw-hdr" style="text-align:left">仕入量</div>'+da.map((a,i)=>{
      const r=a/mxD;const lv=r>.85?'多め':r>.6?'普通':'少なめ';
      const bg=r>.85?'rgba(232,162,48,.7)':r>.6?'rgba(92,184,90,.5)':'rgba(58,52,42,.6)';
      const col=r>.85?'#12100e':'#f0e8d8';
      return `<div class="pw-cell" style="background:${bg};color:${col}" title="${DOW[i]}の平均: ${fY(a)}">${lv}</div>`;
    }).join('')+
    '<div class="pw-hdr" style="text-align:left">平均売上</div>'+da.map(a=>`<div class="pw-cell" style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text2)">${fY(a)}</div>`).join('');
  // 曜日アドバイス
  const topD=da.map((a,i)=>({i,a})).sort((a,b)=>b.a-a.a).slice(0,2);
  $('pu-dow-adv').innerHTML=topD.map(({i,a})=>`
    <div class="adv info"><div class="adv-icon">📅</div><div>
      <div class="adv-t">${DOW[i]}曜が売上ピーク(平均 ${fY(a)})</div>
      <div class="adv-d">${DOW[(i+6)%7]}曜(前日)に仕入れを多めに確保してください。特に主力商品の在庫は通常の1.5倍を目安に。</div>
    </div></div>`).join('');
  // 品切れリスク時間帯
  const hA=new Array(24).fill(0),hC=new Array(24).fill(0);
  rows.filter(r=>r.hour!=null).forEach(r=>{hA[r.hour]+=r.amount;hC[r.hour]++;});
  const hAvg=hA.map((a,i)=>hC[i]?a/hC[i]:0);
  const totH=avg(hAvg.filter(v=>v>0));
  const riskH=hAvg.map((a,i)=>({h:i,a,r:a/(totH||1)})).filter(x=>x.r>1.3&&hC[x.h]>0).sort((a,b)=>b.r-a.r).slice(0,5);
  $('stockout-list').innerHTML=riskH.map(x=>`
    <div style="padding:7px 0;border-bottom:1px solid var(--border)">
      <div style="display:flex;justify-content:space-between;margin-bottom:4px">
        <span style="font-family:'JetBrains Mono',monospace;color:var(--amber)">${x.h}時台</span>
        <span class="badge ${x.r>1.7?'br':'ba'}">${x.r>1.7?'高リスク':'要注意'}</span>
      </div>
      <div class="risk-bw"><div class="risk-b" style="width:${Math.min(100,x.r/2*100)}%;background:${x.r>1.7?'var(--red)':'var(--amber)'}"></div></div>
      <div style="font-size:10px;color:var(--text3);margin-top:2px">${x.h-1>=0?x.h-1+'時までに補充':' 開店前に補充'}推奨(平均需要の${fN(x.r*100,0)}%)</div>
    </div>`).join('')||'<div style="font-size:12px;color:var(--text3)">目立ったリスク時間帯はありません</div>';
  // 商品別仕入れ推奨量
  const bP={};
  rows.forEach(r=>{
    if(!bP[r.product])bP[r.product]={amt:0,qty:0,dates:new Set(),dows:new Array(7).fill(0),dowc:new Array(7).fill(0)};
    bP[r.product].amt+=r.amount;bP[r.product].qty+=r.qty;bP[r.product].dates.add(r.date);
    if(r.dow!=null){bP[r.product].dows[r.dow]+=r.amount;bP[r.product].dowc[r.dow]++;}
  });
  const allD=[...new Set(rows.map(r=>r.date))];
  const totA=sum(Object.values(bP).map(v=>v.amt));
  const prods=Object.entries(bP).map(([n,v])=>{
    const dayAvgQty=v.qty/allD.length;
    const da2=v.dows.map((a,i)=>v.dowc[i]?a/v.dowc[i]:0);
    const pkDow=da2.indexOf(Math.max(...da2));
    const pkMult=da2[pkDow]/(avg(da2.filter(x=>x>0))||1);
    const vol=std(da2.filter(x=>x>0))/(avg(da2.filter(x=>x>0))||1);
    const contrib=v.amt/totA;
    const priority=contrib*.6+(dayAvgQty>.5?.3:.1)+(vol>.4?.1:.05);
    // 推奨仕入れ量: 日平均+バッファ(変動大→+30%、普通→+15%)
    const buf=vol>.4?1.3:vol>.2?1.15:1.1;
    const rec=Math.ceil(dayAvgQty*buf);
    return{n,dayAvgQty,pkDow,pkMult,vol,contrib,priority,rec};
  }).sort((a,b)=>b.priority-a.priority).slice(0,15);
  $('pu-tbody').innerHTML=prods.map(p=>`<tr>
    <td style="font-weight:500">${p.n}</td>
    <td style="font-family:'JetBrains Mono',monospace">${fN(p.dayAvgQty,1)}個</td>
    <td>${DOW[p.pkDow]}曜 <span class="badge ba">×${fN(p.pkMult,1)}</span></td>
    <td style="font-family:'JetBrains Mono',monospace;font-weight:600;color:var(--amber)">${p.rec}個/日</td>
    <td><span class="badge ${p.vol>.4?'br':p.vol>.2?'ba':'bg'}">${p.vol>.4?'変動大':p.vol>.2?'普通':'安定'}</span></td>
    <td><span class="badge ${p.priority>prods[0].priority*.7?'ba':p.priority>prods[0].priority*.4?'bb':'bg'}">${p.priority>prods[0].priority*.7?'最優先':p.priority>prods[0].priority*.4?'優先':'通常'}</span></td>
    <td style="font-size:11px;color:var(--text2)">${DOW[(p.pkDow+6)%7]}曜に${p.rec}個仕入${p.vol>.4?' (変動注意)':''}</td>
  </tr>`).join('');
  // 季節
  const mA2=new Array(12).fill(0),mC2=new Array(12).fill(0);
  rows.forEach(r=>{if(r.month){mA2[r.month-1]+=r.amount;mC2[r.month-1]++;}});
  const mAvg=mA2.map((a,i)=>mC2[i]?a/mC2[i]:0),tmA=avg(mAvg.filter(v=>v>0));
  dc('pu-season');charts['pu-season']=new Chart($('ch-pu-season').getContext('2d'),{type:'bar',data:{labels:MON,datasets:[{label:'月平均売上',data:mAvg,backgroundColor:mAvg.map(v=>v>tmA*1.12?'rgba(232,162,48,.85)':v<tmA*.88?'rgba(74,159,212,.6)':'rgba(90,78,64,.5)'),borderRadius:4,borderWidth:0}]},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>fY(c.raw)}}}}});
  const hiM=mAvg.map((a,i)=>({m:i,a})).filter(x=>x.a>tmA*1.1).sort((a,b)=>b.a-a.a);
  const loM=mAvg.map((a,i)=>({m:i,a})).filter(x=>x.a>0&&x.a<tmA*.9).sort((a,b)=>a.a-b.a);
  const sa=[];
  if(hiM.length)sa.push({t:'warn',i:'📈',tl:`${hiM.map(x=>MON[x.m]).slice(0,3).join('・')}は需要が高い時期`,d:`平均より${fN((hiM[0].a/tmA-1)*100,0)}%多く売れます。1〜2週間前から仕入れを増やし、品切れによる機会損失を防ぎましょう。`});
  if(loM.length)sa.push({t:'info',i:'📉',tl:`${loM.map(x=>MON[x.m]).slice(0,3).join('・')}は閑散期`,d:`売上が平均より${fN((1-loM[0].a/tmA)*100,0)}%低い傾向。仕入れを絞って廃棄ロスを減らすか、この時期に合ったキャンペーンを検討しましょう。`});
  $('pu-season-adv').innerHTML=sa.map(a=>`<div class="adv ${a.t}"><div class="adv-icon">${a.i}</div><div><div class="adv-t">${a.tl}</div><div class="adv-d">${a.d}</div></div></div>`).join('');
}

// ─── アドバイス ──────────────────────────────
function buildAdvice(){
  const rows=filteredRows;
  const advs=[];
  const dates=[...new Set(rows.map(r=>r.date))].sort();
  const totalAmt=sum(rows.map(r=>r.amount));
  const rN=Math.min(7,Math.floor(dates.length/3));
  const rD=dates.slice(-rN),oD=dates.slice(-rN*2,-rN);
  const rA=sum(rows.filter(r=>rD.includes(r.date)).map(r=>r.amount));
  const oA=sum(rows.filter(r=>oD.includes(r.date)).map(r=>r.amount));
  const tr=oA>0?(rA-oA)/oA*100:0;
  if(tr>10)advs.push({t:'good',i:'📈',tl:'売上が伸びています',d:`直近${rN}日間は前の同期間より ${fP(tr)} 増加。何が効いているか記録しておきましょう。`,a:'この成功パターンを記録して、来月の仕入れ計画に活かしてください。'});
  else if(tr<-10)advs.push({t:'bad',i:'⚠️',tl:'売上が下落傾向',d:`直近${rN}日間は前の同期間より ${fP(tr)} 減少。品揃えや接客を見直す時期です。`,a:'主力商品(売上TOP3)だけでも欠品がないか、今すぐ在庫確認を。'});
  // ピーク時間帯
  const hA={};rows.filter(r=>r.hour!=null).forEach(r=>{hA[r.hour]=(hA[r.hour]||0)+r.amount;});
  const pkH=Object.entries(hA).sort((a,b)=>b[1]-a[1])[0];
  if(pkH)advs.push({t:'teal',i:'⏰',tl:`${pkH[0]}時台が最もよく売れる時間帯`,d:`全体売上の ${fN(pkH[1]/totalAmt*100,1)}%。この時間帯の品切れは最大の機会損失につながります。`,a:`${pkH[0]-1>=0?pkH[0]-1:'開店前'}時までに主力商品の補充を完了させましょう。`});
  // 曜日
  const dA=new Array(7).fill(0),dC=new Array(7).fill(0);
  rows.forEach(r=>{if(r.dow!=null){dA[r.dow]+=r.amount;dC[r.dow]++;}});
  const da=dA.map((a,i)=>dC[i]?a/dC[i]:0);
  const pkD=da.indexOf(Math.max(...da)),wkD=da.indexOf(Math.min(...da.filter(v=>v>0)));
  advs.push({t:'info',i:'📅',tl:`${DOW[pkD]}曜日が売上ピーク(平均 ${fY(da[pkD])})`,d:`${DOW[(pkD+6)%7]}曜(前日)に仕入れを多めに。${DOW[wkD]}曜(平均 ${fY(da[wkD])})は仕入れを絞ると廃棄ロスを減らせます。`,a:`${DOW[(pkD+6)%7]}曜の発注量を現在の1.3〜1.5倍に設定してみましょう。`});
  // パレート
  const bP={};rows.forEach(r=>{bP[r.product]=(bP[r.product]||0)+r.amount;});
  const ps=Object.entries(bP).sort((a,b)=>b[1]-a[1]);
  const t20=Math.ceil(ps.length*.2),t20A=sum(ps.slice(0,t20).map(x=>x[1]));
  advs.push({t:'purple',i:'📦',tl:`上位${t20}商品が売上の ${fN(t20A/totalAmt*100,0)}% を占めています`,d:`「${ps.slice(0,3).map(x=>x[0]).join('・')}」が特に重要。これらの品切れが最も痛手になります。`,a:`上位商品の在庫は必ず2日分以上確保するルールを作りましょう。`});
  // 季節
  const mA=new Array(12).fill(0),mC=new Array(12).fill(0);
  rows.forEach(r=>{if(r.month){mA[r.month-1]+=r.amount;mC[r.month-1]++;}});
  const mAvg=mA.map((a,i)=>mC[i]?a/mC[i]:0),tmA=avg(mAvg.filter(v=>v>0));
  const hiM=mAvg.map((a,i)=>({m:i,a})).filter(x=>x.a>tmA*1.12).sort((a,b)=>b.a-a.a);
  const loM=mAvg.map((a,i)=>({m:i,a})).filter(x=>x.a>0&&x.a<tmA*.88).sort((a,b)=>a.a-b.a);
  if(hiM.length)advs.push({t:'warn',i:'🌸',tl:`${hiM.map(x=>MON[x.m]).slice(0,3).join('・')}は繁忙期`,d:`平均より${fN((hiM[0].a/tmA-1)*100,0)}%多く売れる時期。1〜2週間前から仕入れ量を増やしてください。`,a:`${MON[hiM[0].m]}の仕入れは通常の${Math.round((hiM[0].a/tmA)*10)/10}倍を目安に計画しましょう。`});
  if(loM.length)advs.push({t:'info',i:'🍂',tl:`${loM.map(x=>MON[x.m]).slice(0,3).join('・')}は閑散期`,d:`売上が平均より${fN((1-loM[0].a/tmA)*100,0)}%低い傾向。仕入れを絞って廃棄ロスを最小化しましょう。`,a:`この時期はキャンペーンを打つか、仕入れ量を${Math.round((1-(loM[0].a/tmA))*100)}%削減するか選択してください。`});
  // キャンペーン振り返り
  if(campaigns.length){
    const effects=campaigns.map(cp=>{
      const cA=sum(filteredRows.filter(r=>r.date>=cp.start&&r.date<=cp.end).map(r=>r.amount));
      const d1=new Date(cp.start),d2=new Date(cp.end);const days=Math.round((d2-d1)/86400000)+1;
      const pE=new Date(d1);pE.setDate(pE.getDate()-1);const pS=new Date(pE);pS.setDate(pS.getDate()-days+1);
      const pA=sum(filteredRows.filter(r=>r.date>=pS.toISOString().slice(0,10)&&r.date<=pE.toISOString().slice(0,10)).map(r=>r.amount));
      return{name:cp.name,lift:pA>0?(cA-pA)/pA*100:0};
    }).sort((a,b)=>b.lift-a.lift);
    const best=effects[0];
    if(best.lift>5)advs.push({t:'good',i:'🎯',tl:`「${best.name}」が効果的でした`,d:`前回同期間比 ${fP(best.lift)} の売上増。このキャンペーンは成功パターンです。`,a:'同じ時期・内容で来年も実施することを検討してください。仕入れも1.2〜1.5倍に増やすと良いでしょう。'});
    else if(best.lift<-5)advs.push({t:'bad',i:'🎯',tl:'キャンペーン効果が出ていません',d:`「${best.name}」は前回比 ${fP(best.lift)}。告知方法・割引率・対象商品の見直しが必要です。`,a:'次回は割引率を5〜10%上げるか、SNSでの告知を強化してみましょう。'});
  }
  // 客単価
  const txS={};rows.forEach(r=>{const k=r.datetime||r.date;if(!txS[k])txS[k]=0;txS[k]+=r.amount;});
  const txA=Object.values(txS),avgTx=avg(txA);
  if(avgTx<800)advs.push({t:'warn',i:'💰',tl:`客単価 ${fY(avgTx)} — アップセルの余地あり`,d:'「もう1品いかがですか?」の声がけやセットメニューで単価を上げましょう。',a:`客単価を100円上げるだけで、取引${txA.length}件 × 100円 = ${fY(txA.length*100)}の売上増になります。`});
  // サマリー
  $('adv-summary').textContent=`期間: ${dates[0]}〜${dates[dates.length-1]}(${dates.length}日間) 総売上: ${fY(totalAmt)} 取引: ${txA.length}件 客単価: ${fY(avgTx)} 商品種類: ${ps.length}種`;
  // 描画(アクションボックス付き)
  $('adv-list').innerHTML=advs.map(a=>`
    <div class="adv ${a.t}">
      <div class="adv-icon">${a.i}</div>
      <div style="flex:1">
        <div class="adv-t">${a.tl}</div>
        <div class="adv-d">${a.d}</div>
        ${a.a?`<div class="action-box">▶ 次のアクション: ${a.a}</div>`:''}
      </div>
    </div>`).join('');
  const badge=$('adv-badge');badge.textContent=advs.length;badge.style.display='inline-flex';
  $('emp-advice').style.display='none';$('adv-content').style.display='block';
}

// ── init ────────────────────────────────────────
log('「サンプル」ボタンでデモを確認できます','info');
log('Square・Airレジ・ExcelのCSVを直接ドロップできます','info');
</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
お店の売上分析ツール「Mise」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1