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

【更新履歴】

 ・2026/2/20 バージョン1.0公開。
 ・2026/2/20 バージョン1.1公開。
 ・2026/2/20 バージョン1.2公開。(MisePOSに対応)

画像
画像
画像


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

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

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MISE 1.2 — 小さなお店の販売分析</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&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
  /* MISE POS Theme Colors */
  --bg0:     #fff8f2;
  --bg1:     #ffffff;
  --bg2:     #fff2e6;
  --bg3:     #ffe4d9;
  --border:  #f0d8c8;
  --border2: #ff8c69;
  
  --text0:   #3d2c22;
  --text1:   #5a4e40;
  --text2:   #8a7a70;
  --text3:   #aaa09a;
  
  --amber:   #ff8c69; /* peach */
  --amber-l: #ffe4d9;
  --amber-d: #e57e5e;
  
  --green:   #6bcb8b; /* mint */
  --green-l: #d9f5e4;
  
  --red:     #ff6b6b; /* coral */
  --red-l:   #ffd9d9;
  
  --blue:    #5bb8f5; /* sky */
  --blue-l:  #d9f0ff;
  
  --purple:  #b388f7; /* purple */
  --purple-l:#ede0ff;
  
  --teal:    #6bcb8b; /* fallback to mint */
  --teal-l:  #d9f5e4;
  
  --r: 12px;
  --sh: 0 4px 18px rgba(255,100,50,.10);
}

*,*::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;}

/* Scanline noise overlay to match POS */
body::before{content:'';position:fixed;inset:0;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Crect width='1' height='1' fill='%23ff8c69' opacity='.03'/%3E%3C/svg%3E");pointer-events:none;z-index:999;}

::-webkit-scrollbar{width:6px;height:6px;}
::-webkit-scrollbar-track{background:var(--bg2);}
::-webkit-scrollbar-thumb{background:var(--amber);border-radius:4px;}

/* ── Header ── */
header{position:sticky;top:0;z-index:300;background:linear-gradient(90deg,var(--amber) 0%,var(--red) 100%);color:#fff;display:flex;align-items:center;gap:16px;padding:0 20px;height:52px;box-shadow:0 3px 14px rgba(255,100,50,.25);}
.logo{font-family:'Nunito',sans-serif;font-size:20px;font-weight:900;letter-spacing:.04em;color:#fff;}
.logo-sub{font-size:11px;font-weight:700;color:rgba(255,255,255,.85);letter-spacing:.1em;border-left:1px solid rgba(255,255,255,.3);padding-left:14px;white-space:nowrap;}
.hdr-r{margin-left:auto;display:flex;align-items:center;gap:10px;}
#store-disp{font-size:12px;font-weight:700;color:rgba(255,255,255,.95) !important;}
.hkpi{display:flex;flex-direction:column;align-items:flex-end;padding:3px 12px;background:rgba(255,255,255,.2);border-radius:var(--r);}
.hkpi-l{font-size:9px;color:rgba(255,255,255,.8);letter-spacing:.1em;text-transform:uppercase;font-weight:700;}
.hkpi-v{font-family:'Nunito',monospace;font-size:14px;font-weight:800;color:#fff;}

/* ── Layout ── */
.layout{display:grid;grid-template-columns:272px 1fr;min-height:calc(100vh - 52px);}
.side{background:var(--bg1);border-right:2px solid var(--border);overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px;position:relative;z-index:10;}
.ss{border-bottom:2px solid var(--border);padding-bottom:12px;}
.ss:last-child{border-bottom:none;}
.sl{font-size:10px;font-weight:700;letter-spacing:.1em;color:var(--text2);margin-bottom:7px;}

/* forms */
.f{display:flex;flex-direction:column;gap:3px;margin-bottom:7px;}
.f label{font-size:11px;color:var(--text2);font-weight:700;}
input[type=text],input[type=number],input[type=date],select,textarea{background:var(--bg0);border:2px solid var(--border);border-radius:10px;padding:6px 10px;font-family:inherit;font-weight:500;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(--bg1);}
textarea{resize:vertical;min-height:50px;font-size:11px;}

.btn{display:flex;align-items:center;justify-content:center;gap:5px;padding:8px 12px;border-radius:10px;font-family:inherit;font-size:12px;font-weight:700;cursor:pointer;border:none;width:100%;transition:all .15s;white-space:nowrap;}
.btn-amber{background:linear-gradient(135deg,var(--amber),var(--red));color:#fff;box-shadow:0 3px 10px rgba(255,100,50,.2);}
.btn-amber:hover{transform:translateY(-2px);box-shadow:0 5px 14px rgba(255,100,50,.3);}
.btn-outline{background:var(--bg1);color:var(--text1);border:2px solid var(--border);}
.btn-outline:hover{border-color:var(--amber);color:var(--amber);background:var(--amber-l);}
.btn-green{background:var(--green);color:#fff;}
.btn-green:hover{background:#5bb37b;}
.btn-sm{padding:6px 10px;font-size:11px;width:auto;}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}

.drop-zone{border:3px dashed var(--border);border-radius:14px;padding:16px 10px;text-align:center;cursor:pointer;color:var(--text2);font-size:12px;background:var(--bg0);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(--bg1);border:2px solid var(--border);border-radius:10px;padding:6px 8px;max-height:68px;overflow-y:auto;}
#log div{margin-bottom:1px;}
#log .ok{color:var(--green);font-weight:600;}#log .err{color:var(--red);font-weight:600;}#log .info{color:var(--blue);}#log .warn{color:var(--amber);font-weight:600;}

/* campaign list */
.cp-list{display:flex;flex-direction:column;gap:5px;max-height:120px;overflow-y:auto;}
.cp-item{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg1);border:2px solid var(--border);border-radius:10px;font-size:11px;}
.cp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;}
.cp-name{flex:1;color:var(--text0);font-weight:600;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;font-size:14px;line-height:1;}.cp-del:hover{color:var(--red);}

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

/* Banner */
.banner{background:linear-gradient(135deg,var(--bg1) 0%,var(--bg2) 100%);border-bottom:2px solid var(--border);padding:16px 24px;display:grid;grid-template-columns:auto 1fr repeat(3,auto);gap:16px;align-items:center;}
.banner.hidden{display:none;}
.s-ring{width:48px;height:48px;border-radius:50%;border:3px solid var(--border);background:var(--bg1);display:flex;align-items:center;justify-content:center;font-size:24px;transition:all .4s;}
.s-ring.good{border-color:var(--green);background:var(--green-l);box-shadow:0 0 14px rgba(107,203,139,.3);}
.s-ring.warn{border-color:var(--amber);background:var(--amber-l);box-shadow:0 0 14px rgba(255,140,105,.3);}
.s-ring.bad {border-color:var(--red);  background:var(--red-l);  box-shadow:0 0 14px rgba(255,107,107,.3);}
.s-lbl{font-size:10px;font-weight:700;color:var(--text2);letter-spacing:.1em;text-align:center;margin-top:4px;}
.b-msg{}
.b-hl{font-size:16px;font-weight:700;color:var(--text0);line-height:1.4;}
.b-dt{font-size:12px;color:var(--text2);margin-top:2px;}
.bkpi{display:flex;flex-direction:column;align-items:flex-end;padding:8px 14px;background:var(--bg1);border:2px solid var(--border);border-radius:12px;min-width:110px;}
.bkpi-l{font-size:10px;font-weight:700;color:var(--text2);letter-spacing:.1em;text-transform:uppercase;}
.bkpi-v{font-family:'Nunito',monospace;font-size:20px;font-weight:800;color:var(--text0);}
.bkpi-v.up{color:var(--green);}.bkpi-v.down{color:var(--red);}
.bkpi-s{font-size:11px;font-weight:600;color:var(--text3);}

/* Tabs */
.tab-bar{display:flex;overflow-x:auto;background:var(--bg1);border-bottom:2px solid var(--border);padding:0 20px;position:sticky;top:52px;z-index:200;}
.tab-bar::-webkit-scrollbar{height:3px;}
.tab{padding:12px 16px;font-size:13px;cursor:pointer;color:var(--text2);border-bottom:3px solid transparent;white-space:nowrap;transition:all .15s;font-weight:700;display:flex;align-items:center;gap:6px;}
.tab:hover{color:var(--amber);}
.tab.active{color:var(--amber);border-bottom-color:var(--amber);}
.tbadge{background:var(--red);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;font-family:'Nunito',monospace;font-weight:800;}

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

/* Card */
.card{background:var(--bg1);border:2px solid var(--border);border-radius:16px;padding:18px;margin-bottom:16px;box-shadow:0 2px 8px rgba(0,0,0,.02);}
.card-hd{display:flex;align-items:baseline;gap:10px;margin-bottom:4px;}
.card-t{font-size:14px;font-weight:800;color:var(--text0);}
.card-sub{font-size:12px;color:var(--text2);margin-bottom:14px;font-weight:500;}
.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:16px;}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;}

/* Advice */
.adv-list{display:flex;flex-direction:column;gap:10px;}
.adv{display:flex;gap:12px;padding:14px 16px;border-radius:14px;border-left:5px solid var(--border);background:var(--bg1);border-top:2px solid var(--border);border-right:2px solid var(--border);border-bottom:2px solid var(--border);animation:fu .3s ease;}
@keyframes fu{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
.adv.good{border-left-color:var(--green);background:var(--green-l);border-color:rgba(107,203,139,.3);border-left-width:5px;}
.adv.warn{border-left-color:var(--amber);background:var(--amber-l);border-color:rgba(255,140,105,.3);border-left-width:5px;}
.adv.bad {border-left-color:var(--red);  background:var(--red-l);  border-color:rgba(255,107,107,.3);border-left-width:5px;}
.adv.info{border-left-color:var(--blue); background:var(--blue-l); border-color:rgba(91,184,245,.3);border-left-width:5px;}
.adv.purple{border-left-color:var(--purple);background:var(--purple-l);border-color:rgba(179,136,247,.3);border-left-width:5px;}
.adv.teal  {border-left-color:var(--teal);  background:var(--teal-l);  border-color:rgba(107,203,139,.3);border-left-width:5px;}
.adv-icon{font-size:22px;flex-shrink:0;}
.adv-t{font-weight:800;font-size:14px;color:var(--text0);margin-bottom:4px;}
.adv-d{font-size:13px;color:var(--text1);line-height:1.6;}
/* アクションボックス */
.action-box{margin-top:8px;padding:10px 12px;background:var(--bg1);border-radius:10px;font-size:12px;color:var(--text0);font-weight:700;border-left:3px solid var(--amber);box-shadow:0 2px 6px rgba(0,0,0,.03);}

/* Heatmap */
.hm-wrap{overflow-x:auto;}
.heatmap{display:grid;grid-template-columns:48px repeat(24,1fr);gap:3px;min-width:700px;}
.hm-cell{height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:9px;font-family:'Nunito',monospace;font-weight:700;cursor:default;transition:transform .15s;position:relative;}
.hm-cell:hover{transform:scale(1.15);z-index:2;box-shadow:0 4px 8px rgba(0,0,0,.15);}
.hm-lbl{background:transparent;font-size:11px;font-weight:700;color:var(--text2);justify-content:flex-start;}
.hm-hdr{background:transparent;font-size:10px;font-weight:700;color:var(--text3);height:20px;}

/* Rank table */
.rtbl{width:100%;border-collapse:collapse;font-size:13px;}
.rtbl th{font-size:11px;font-weight:800;color:var(--text2);text-align:left;padding:8px 10px;border-bottom:2px solid var(--border);white-space:nowrap;}
.rtbl td{padding:10px;border-bottom:1px solid var(--border);vertical-align:middle;}
.rtbl tr:hover td{background:var(--bg0);}
.rbar-w{height:6px;background:var(--bg2);border-radius:4px;min-width:70px;overflow:hidden;}
.rbar{height:100%;border-radius:4px;transition:width .6s;}
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:10px;font-size:11px;font-weight:800;white-space:nowrap;}
.bg{background:var(--green-l);color:var(--green);border:1px solid rgba(107,203,139,.3);}
.br{background:var(--red-l);color:var(--red);border:1px solid rgba(255,107,107,.3);}
.ba{background:var(--amber-l);color:var(--amber);border:1px solid rgba(255,140,105,.3);}
.bb{background:var(--blue-l);color:var(--blue);border:1px solid rgba(91,184,245,.3);}
.bp{background:var(--purple-l);color:var(--purple);border:1px solid rgba(179,136,247,.3);}
.bt{background:var(--teal-l);color:var(--teal);border:1px solid rgba(107,203,139,.3);}

/* Campaign effect */
.ce-wrap{display:grid;grid-template-columns:1fr auto 1fr;gap:0;align-items:center;margin-bottom:16px;}
.ce-box{padding:16px;text-align:center;border:2px solid var(--border);border-radius:14px;background:var(--bg0);}
.ce-arr{display:flex;flex-direction:column;align-items:center;padding:0 14px;}
.ce-chg{font-family:'Nunito',monospace;font-weight:900;font-size:20px;}
.ce-lbl{font-size:11px;font-weight:700;color:var(--text2);margin-bottom:6px;}
.ce-val{font-family:'Nunito',monospace;font-size:24px;font-weight:900;color:var(--text0);}

/* Purchase */
.pw-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;margin-top:12px;}
.pw-hdr{text-align:center;font-size:11px;font-weight:700;color:var(--text2);padding:4px;}
.pw-cell{padding:8px 4px;text-align:center;border-radius:8px;font-size:11px;font-weight:800;cursor:default;transition:transform .15s;}
.pw-cell:hover{transform:scale(1.08);}
.risk-bw{height:8px;background:var(--bg2);border-radius:4px;overflow:hidden;}
.risk-b{height:100%;border-radius:4px;transition:width .8s;}

/* Quick entry */
.qe-btn{padding:14px 6px;background:var(--bg0);border:2px solid var(--border);border-radius:12px;text-align:center;cursor:pointer;font-size:12px;font-weight:700;color:var(--text1);transition:all .15s;}
.qe-btn:hover{border-color:var(--amber);color:var(--amber);background:var(--amber-l);}

/* Format modal */
.modal-bg{display:none;position:fixed;inset:0;background:rgba(60,40,30,.6);backdrop-filter:blur(4px);z-index:500;align-items:center;justify-content:center;animation:fadeIn .2s;}
@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
.modal-bg.show{display:flex;}
.modal{background:var(--bg1);border-radius:20px;padding:28px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:0 16px 40px rgba(0,0,0,.15);animation:slideUp .25s;}
@keyframes slideUp{from{transform:translateY(30px);opacity:0;}to{transform:translateY(0);opacity:1;}}
.modal h3{font-size:18px;font-weight:800;color:var(--amber);margin-bottom:16px;}
.modal code{font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg0);border:1px solid var(--border);padding:10px 14px;display:block;border-radius:10px;margin:8px 0;color:var(--text0);white-space:pre;overflow-x:auto;}
.modal .close{float:right;cursor:pointer;color:var(--text2);font-size:22px;line-height:1;}

/* Empty */
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:280px;color:var(--text2);gap:12px;text-align:center;font-weight:700;}
.empty-icon{font-size:48px;opacity:.8;}

/* Loading */
#loading{display:none;position:fixed;inset:0;background:rgba(255,248,242,.85);backdrop-filter:blur(4px);z-index:999;align-items:center;justify-content:center;flex-direction:column;gap:16px;}
#loading.show{display:flex;}
.lr{width:40px;height:40px;border:4px 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:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px;padding:12px 16px;background:var(--bg1);border:2px solid var(--border);border-radius:14px;}
.frow label{font-size:12px;font-weight:700;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:10px;margin-bottom:8px;}
.colmap-row label{font-size:12px;font-weight:700;color:var(--text1);width:64px;flex-shrink:0;}
.colmap-row select{flex:1;}
.colmap-preview{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--teal);margin-top:6px;padding:8px 10px;background:var(--bg0);border:1px solid var(--teal-l);border-radius:8px;}

@media(max-width:900px){
  .layout{grid-template-columns:1fr;}
  .side{border-right:none;border-bottom:2px 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:14px;font-weight:700;color:var(--text1)">分析中...</div></div>

<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:13px;color:var(--text1);margin-bottom:16px;font-weight:500;">以下のいずれかの形式に対応しています。ヘッダー行の有無は自動判定します。</p>
    <p style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:4px">▶ 最小構成(日付と金額だけ)</p>
    <code>日付,金額
2024-01-15,1500
2024-01-15,980</code>
    <p style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:4px;margin-top:14px">▶ 標準(商品名付き)</p>
    <code>日時,商品名,数量,金額
2024-01-15 12:30,コーヒー,2,760
2024-01-15 13:05,ランチ,1,850</code>
    <p style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:4px;margin-top:14px">▶ 詳細(カテゴリ付き)</p>
    <code>日時,商品名,カテゴリ,数量,金額
2024-01-15 12:30,コーヒー,ドリンク,2,760</code>
    <p style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:4px;margin-top:14px">▶ MISE POS形式(顧客情報付き)</p>
    <code>txid,datetime,date,hour,dow,month,product_id,product,category,price,quantity,amount,tax,discount_pct,customer_type,age_band,note
TX0001,2024-01-15 12:30:00,2024-01-15,12,1,1,P001,カフェラテ,ドリンク,495,1,495,10,0,member,30-39,</code>
    <p style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:4px;margin-top:14px">▶ 対応POSアプリ</p>
    <p style="font-size:13px;color:var(--text1);line-height:1.7;"><b>Square:</b> 「レポート」→「売上サマリー」→「エクスポート」<br>
    <b>Airレジ:</b> 「データ管理」→「売上データ」→「CSV出力」<br>
    <b>Excel:</b> 「名前を付けて保存」→「CSV (コンマ区切り)」</p>
    <p style="font-size:11px;font-weight:700;color:var(--text3);margin-top:16px">※ 列の順番は問いません。列名から自動で判定します。判定できない場合は手動で設定できます。</p>
  </div>
</div>

<header>
  <div class="logo">MISE</div>
  <div class="logo-sub">小さなお店の販売分析</div>
  <div class="hdr-r">
    <span id="store-disp">データ未読み込み</span>
    <div id="hdr-kpis" style="display:none;display:flex;gap:10px">
      <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">
<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:28px;margin-bottom:8px">📂</div>
      <div style="font-weight:800;color:var(--text1);">CSVをドロップ / クリック</div>
      <div style="font-size:11px;margin-top:6px;color:var(--text3);line-height:1.6;font-weight:700;">MISE POS・Square・Airレジ対応<br>日時 / 商品名 / 数量 / 金額</div>
    </div>
    <div style="display:flex;gap:8px;margin-top:10px">
      <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:10px">適用して分析</button>
  </div>

  <div class="ss">
    <div class="sl">手入力(紙帳簿から)</div>
    <div style="display:flex;gap:8px;margin-bottom:8px">
      <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 class="f"><label>顧客種別</label>
        <select id="qe-ctype">
          <option value="">未記録</option>
          <option value="general">一般</option>
          <option value="member">会員</option>
          <option value="student">学生</option>
          <option value="senior">シニア</option>
          <option value="staff">スタッフ</option>
        </select>
      </div>
      <div class="f"><label>年齢層</label>
        <select id="qe-age">
          <option value="">未記録</option>
          <option value="〜19">〜19歳</option>
          <option value="20-29">20-29歳</option>
          <option value="30-39">30-39歳</option>
          <option value="40-49">40-49歳</option>
          <option value="50-59">50-59歳</option>
          <option value="60+">60歳以上</option>
        </select>
      </div>
      <div style="display:flex;gap:8px;margin-top:10px;">
        <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:8px;margin-top:10px;">
        <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>
    <div class="f"><label>顧客種別</label>
      <select id="fCustType">
        <option value="all">すべて</option>
        <option value="general">一般</option>
        <option value="member">会員</option>
        <option value="student">学生</option>
        <option value="senior">シニア</option>
        <option value="staff">スタッフ</option>
      </select>
    </div>
    <div class="f"><label>年齢層</label>
      <select id="fAgeBand">
        <option value="all">すべて</option>
        <option value="〜19">〜19歳</option>
        <option value="20-29">20-29歳</option>
        <option value="30-39">30-39歳</option>
        <option value="40-49">40-49歳</option>
        <option value="50-59">50-59歳</option>
        <option value="60+">60歳以上</option>
      </select>
    </div>
    <button class="btn btn-outline" onclick="applyFilter()" style="margin-top:6px">絞り込む</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()" style="margin-top:4px">登録</button>
    <div class="cp-list" id="cp-list" style="margin-top:10px"></div>
  </div>

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

</div><div class="main">

<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:14px" 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>

<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('customer',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>

<div class="tab-content active" id="tab-overview">
  <div class="empty" id="emp-overview"><div class="empty-icon">📊</div><div>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:10px">商品</label>
      <select id="ov-prod" onchange="buildOverview()" style="max-width:160px"><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>

<div class="tab-content" id="tab-heatmap">
  <div class="empty" id="emp-heatmap"><div class="empty-icon">🕐</div><div>時間付きデータが必要です(例: 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:10px">商品</label>
      <select id="hm-prod" onchange="buildHeatmap()" style="max-width:160px"><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:10px"></div></div>
    </div>
  </div>
</div>

<div class="tab-content" id="tab-products">
  <div class="empty" id="emp-products"><div class="empty-icon">📦</div><div>商品名付きのデータが必要です</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:10px">カテゴリ</label>
      <select id="pr-cat" onchange="buildProducts()" style="max-width:140px"><option value="all">すべて</option></select>
      <label style="margin-left:10px">件数</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:40px">#</th><th>商品名</th><th>カテゴリ</th><th>売上金額</th><th style="min-width:110px">構成比</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>

<div class="tab-content" id="tab-campaign">
  <div class="empty" id="emp-campaign"><div class="empty-icon">🎯</div><div>左パネルでキャンペーンを登録してください</div><div style="font-size:12px;color:var(--text3);margin-top:4px;">名称・開始日・終了日を入れるだけで自動比較</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>

<div class="tab-content" id="tab-purchase">
  <div class="empty" id="emp-purchase"><div class="empty-icon">🛒</div><div>データを読み込むと仕入れ提案が表示されます</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:14px;display:flex;flex-direction:column;gap:10px"></div>
      </div>
      <div class="card">
        <div class="card-t">🕐 品切れリスクの高い時間帯</div>
        <div class="card-sub">需要が急増する時間帯。事前に補充を</div>
        <div id="stockout-list" style="margin-top:10px;display:flex;flex-direction:column;gap:10px"></div>
      </div>
    </div>
    <div class="card">
      <div class="card-t">📦 商品別 仕入れ推奨量(1日あたり)</div>
      <div class="card-sub">売れ行きの速さと売上貢献度から算出。変動が大きい商品は多めに確保を。</div>
      <div style="overflow-x:auto;margin-top:12px">
        <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:14px;display:flex;flex-direction:column;gap:10px"></div>
    </div>
  </div>
</div>

<div class="tab-content" id="tab-customer">
  <div class="empty" id="emp-customer"><div class="empty-icon">👤</div><div>MISE POSで顧客種別・年齢層を入力したデータが必要です</div><div style="font-size:12px;color:var(--text3);margin-top:4px;">customer_type / age_band 列が含まれるCSVを読み込んでください</div></div>
  <div id="ct-content" style="display:none">
    <div class="frow">
      <label>集計</label>
      <select id="ct-metric" onchange="buildCustomer()"><option value="amount">売上金額</option><option value="qty">販売数量</option><option value="tx">取引件数</option></select>
    </div>
    <div class="g2">
      <div class="card">
        <div class="card-t">顧客種別 売上構成</div>
        <div class="card-sub">どの顧客層が売上を支えているか</div>
        <canvas id="ch-cust-type"></canvas>
      </div>
      <div class="card">
        <div class="card-t">年齢層 売上構成</div>
        <div class="card-sub">どの年代がよく購入するか</div>
        <canvas id="ch-cust-age"></canvas>
      </div>
    </div>
    <div class="g2">
      <div class="card">
        <div class="card-t">顧客種別 × 曜日 売上ヒートマップ</div>
        <div class="card-sub">何曜日にどの客層が来るか</div>
        <div id="ct-dow-heatmap" style="overflow-x:auto;margin-top:12px;"></div>
      </div>
      <div class="card">
        <div class="card-t">顧客種別 × 時間帯 売上</div>
        <div class="card-sub">時間帯ごとの客層の違い</div>
        <canvas id="ch-cust-hour"></canvas>
      </div>
    </div>
    <div class="card">
      <div class="card-t">顧客種別別 人気商品 TOP5</div>
      <div class="card-sub">各客層が好む商品。仕入れ・提案のヒントに</div>
      <div id="ct-prod-table" style="overflow-x:auto;margin-top:12px;"></div>
    </div>
    <div class="g2">
      <div class="card">
        <div class="card-t">客単価(顧客種別別)</div>
        <div class="card-sub">どの客層が多く使うか</div>
        <canvas id="ch-cust-atv"></canvas>
      </div>
      <div class="card">
        <div class="card-t">割引利用状況</div>
        <div class="card-sub">割引がどの客層に使われているか</div>
        <canvas id="ch-cust-disc"></canvas>
      </div>
    </div>
  </div>
</div>

<div class="tab-content" id="tab-advice">
  <div class="empty" id="emp-advice"><div class="empty-icon">💡</div><div>データを読み込むと自動でアドバイスが表示されます</div></div>
  <div id="adv-content" style="display:none">
    <div class="card" style="margin-bottom:16px">
      <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></div><script>
/* ══════════════════════════════════════════════
   MISE v2 — 分析エンジン
   対応: Square/Airレジ/Excel/MISE POS/手入力
   ══════════════════════════════════════════════ */
let allRows=[], filteredRows=[], campaigns=[], colMap={}, csvHeaders=[];
const CP_COLORS=['#ff8c69','#6bcb8b','#5bb8f5','#b388f7','#ff6b6b','#ffd166'];
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 (Bright Theme)
const CD={
  responsive:true,maintainAspectRatio:true,animation:{duration:450},
  plugins:{
    legend:{labels:{color:'#8a7a70',font:{family:"'IBM Plex Sans JP'",size:11,weight:'600'},boxWidth:12}},
    tooltip:{backgroundColor:'rgba(255,255,255,.95)',borderColor:'#f0d8c8',borderWidth:2,
      titleColor:'#3d2c22',bodyColor:'#8a7a70',padding:10,
      titleFont:{family:"'Nunito'",size:11,weight:'800'},bodyFont:{family:"'Nunito'",size:12,weight:'700'}}
  },
  scales:{
    x:{ticks:{color:'#8a7a70',font:{family:"'Nunito'",size:10,weight:'700'},maxTicksLimit:14},grid:{color:'rgba(240,216,200,.6)'},border:{color:'#f0d8c8'}},
    y:{ticks:{color:'#8a7a70',font:{family:"'Nunito'",size:10,weight:'700'}},grid:{color:'rgba(240,216,200,.6)'},border:{color:'#f0d8c8'}}
  }
};
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){
  if(headers.some(h=>/^(Date|Time)$/i.test(h))&&headers.some(h=>/^(Item|Description)$/i.test(h))) return 'square';
  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,customer_type:-1,age_band:-1,discount_pct:-1,txid:-1};
  const fmt=detectPOSFormat(headers);
  if(fmt==='square'){
    m._squareDate=headers.findIndex(h=>/^Date$/i.test(h));
    m._squareTime=headers.findIndex(h=>/^Time$/i.test(h));
    m.datetime=m._squareDate; 
    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;
  }
  
  // 汎用マッピング (1回目の走査:厳密な一致)
  headers.forEach((h,i)=>{
    if(m.datetime<0&&/datetime|売上日時|日時|timestamp/i.test(h)) m.datetime=i;
    if(m.product <0&&/product|商品|品名|item|メニュー/i.test(h)) m.product=i;
    if(m.category<0&&/category|カテゴリ|分類|部門/i.test(h)) m.category=i;
    if(m.qty     <0&&/quantity|qty|数量|個数|点数/i.test(h)) m.qty=i;
    // MISE POSの price と amount の競合を防ぐため、合計やamountを優先
    if(m.amount  <0&&/amount|金額|売上|revenue|合計|total|gross|net/i.test(h)) m.amount=i;
    if(m.customer_type<0&&/customer_type|客層種別|顧客/i.test(h)) m.customer_type=i;
    if(m.age_band<0&&/age_band|age|年齢/i.test(h)) m.age_band=i;
    if(m.discount_pct<0&&/discount_pct|discount|割引/i.test(h)) m.discount_pct=i;
    if(m.txid<0&&/txid|取引番号|transaction/i.test(h)) m.txid=i;
  });
  
  // 汎用マッピング (2回目の走査:見つからなかった場合のフォールバック)
  headers.forEach((h,i)=>{
    if(m.datetime<0&&/date|日付|time/i.test(h)) m.datetime=i;
    if(m.product <0&&/name/i.test(h)) m.product=i;
    if(m.category<0&&/genre/i.test(h)) m.category=i;
    if(m.amount  <0&&/price|小計/i.test(h)) m.amount=i; // ここで初めてpriceを許容
  });
  
  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]);
    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]||'未分類'):'未分類';
    const customer_type=m.customer_type>=0?(cells[m.customer_type]||''):'';
    const age_band=m.age_band>=0?(cells[m.age_band]||''):'';
    const discount_pct=m.discount_pct>=0?parseFloat(cells[m.discount_pct])||0:0;
    const txid=m.txid>=0?(cells[m.txid]||''):'';
    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,
      customer_type,age_band,discount_pct,txid};
  }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;
  const custType=$('fCustType')?$('fCustType').value:'all';
  const ageBand=$('fAgeBand')?$('fAgeBand').value:'all';
  filteredRows=allRows.filter(r=>{
    if(s&&r.date<s)return false;
    if(e&&r.date>e)return false;
    if(custType!=='all'&&r.customer_type&&r.customer_type!==custType)return false;
    if(ageBand!=='all'&&r.age_band&&r.age_band!==ageBand)return false;
    return true;
  });
  log(`絞り込み: ${filteredRows.length}件`+(custType!=='all'?` [顧客:${custType}]`:'')+( ageBand!=='all'?` [年齢:${ageBand}]`:''),'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;
  const customer_type=$('qe-ctype')?$('qe-ctype').value:'';
  const age_band=$('qe-age')?$('qe-age').value:'';
  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,
    customer_type,age_band,discount_pct:0,txid:''};
  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:'#ff8c69'},
    {name:'夏フェア',start:'2024-07-15',end:'2024-08-15',color:'#6bcb8b'},
  ];
  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),
          customer_type: Math.random()>.5?'general':'member', age_band: '30-39'
        });
      });
    });
  }
  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();buildCustomer();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!=='不明');
  const hasCust=filteredRows.some(r=>r.customer_type&&r.customer_type!=='');
  [['emp-overview','ov-content',true],['emp-heatmap','hm-content',hasT],
   ['emp-products','pr-content',hasP],['emp-customer','ct-content',hasCust],
   ['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(255,140,105,.3)');g.addColorStop(1,'rgba(255,140,105,.01)');
  charts['ov']=new Chart(ctx,{type:'line',data:{labels,datasets:[
    {label:'売上',data:amts,borderColor:'#ff8c69',borderWidth:3,backgroundColor:g,fill:true,tension:.3,pointRadius:amts.length>60?0:4,pointBackgroundColor:'#ff8c69'},
    {label:`${maN}期移動平均`,data:ma,borderColor:'#aaa09a',borderWidth:2,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(255,140,105,.9)':v>=mxD*.7?'rgba(255,140,105,.6)':'rgba(240,216,200,.8)'),borderRadius:6,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(107,203,139,.8)':v<totM*.9?'rgba(255,107,107,.7)':'rgba(91,184,245,.6)'),borderRadius:6,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(--bg2)':`rgba(255,140,105,${.15+r2*.85})`;
      const col=r2>.6?'#ffffff':'var(--text0)';
      const lbl=r2>.35?(metric==='amount'?`${Math.round(v/1000)}k`:Math.round(v)):'';
      html+=`<div class="hm-cell" style="background:${bg};color:${col}" 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(255,140,105,.9)':'rgba(240,216,200,.8)'),borderRadius:4,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:10px"><div style="font-size:12px;font-weight:800;color:var(--amber);font-family:'Nunito',monospace;margin-bottom:4px">▶ ${h}時台</div>${ps.map((p,i)=>`<div style="display:flex;justify-content:space-between;font-size:12px;font-weight:700;color:var(--text1);padding:3px 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)}`;
  
  $('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:'Nunito',monospace;font-size:16px;font-weight:900;color:${tc}">${i+1}</span></td>
      <td style="font-weight:700">${p.n} ${badge}</td>
      <td><span class="badge bp">${p.cat}</span></td>
      <td style="font-family:'Nunito',monospace;font-weight:800">${fY(p.amt)}</td>
      <td><div style="display:flex;align-items:center;gap:8px"><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:'Nunito',monospace;font-size:11px;font-weight:800;color:var(--text2);width:36px;text-align:right">${fN(pct,1)}%</span></div></td>
      <td style="font-family:'Nunito',monospace;font-weight:700">${fN(p.qty)}</td>
      <td style="font-family:'Nunito',monospace;font-weight:700">${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:CP_COLORS.slice(0,cl.length),borderColor:'var(--bg1)',borderWidth:3}]},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:CP_COLORS[i],borderWidth:2.5,pointRadius:3,fill:false,tension:.3}))},options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.dataset.label+': '+fY(c.raw)}}}}});
}

// ─── 顧客分析 ────────────────────────────────
function buildCustomer(){
  const rows=filteredRows;
  const hasCust=rows.some(r=>r.customer_type&&r.customer_type!=='');
  if(!hasCust)return;

  const metric=$('ct-metric')?$('ct-metric').value:'amount';
  const metricFn=(r)=>metric==='amount'?r.amount:metric==='qty'?r.qty:1;

  const CUST_LABELS={general:'一般',member:'会員',student:'学生',senior:'シニア',staff:'スタッフ','':{label:'不明'}};

  // 顧客種別ごとの集計
  const byCust={};
  rows.forEach(r=>{
    const k=r.customer_type||'未記録';
    if(!byCust[k]){byCust[k]={amt:0,qty:0,tx:0,txAmts:[],discAmts:0,discCnt:0,dows:new Array(7).fill(0),hours:new Array(24).fill(0),products:{}};}
    byCust[k].amt+=r.amount;
    byCust[k].qty+=r.qty;
    byCust[k].tx++;
    byCust[k].txAmts.push(r.amount);
    if(r.discount_pct&&r.discount_pct>0){byCust[k].discCnt++;byCust[k].discAmts+=r.amount;}
    if(r.dow!=null)byCust[k].dows[r.dow]+=metricFn(r);
    if(r.hour!=null)byCust[k].hours[r.hour]+=metricFn(r);
    const pkey=r.product||'不明';
    byCust[k].products[pkey]=(byCust[k].products[pkey]||0)+r.amount;
  });

  // 年齢層ごとの集計
  const byAge={};
  rows.forEach(r=>{
    const k=r.age_band||'未記録';
    if(!byAge[k])byAge[k]={amt:0,qty:0,tx:0};
    byAge[k].amt+=r.amount;byAge[k].qty+=r.qty;byAge[k].tx++;
  });

  // ─ 顧客種別 円グラフ ─
  const custKeys=Object.keys(byCust).filter(k=>k!=='未記録'||byCust['未記録'].tx>0);
  const custVals=custKeys.map(k=>metric==='amount'?byCust[k].amt:metric==='qty'?byCust[k].qty:byCust[k].tx);
  dc('cust-type');charts['cust-type']=new Chart($('ch-cust-type').getContext('2d'),{
    type:'doughnut',
    data:{labels:custKeys.map(k=>CUST_LABELS[k]||k),datasets:[{data:custVals,backgroundColor:CP_COLORS.slice(0,custKeys.length),borderColor:'var(--bg1)',borderWidth:3}]},
    options:{responsive:true,maintainAspectRatio:true,plugins:{
      legend:{...CD.plugins.legend,position:'right'},
      tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>`${c.label}: ${metric==='amount'?fY(c.raw):fN(c.raw)}(${fN(c.raw/sum(custVals)*100,1)}%)`}}
    }}
  });

  // ─ 年齢層 棒グラフ ─
  const ageOrder=['〜19','20-29','30-39','40-49','50-59','60+','未記録'];
  const ageKeys=ageOrder.filter(k=>byAge[k]);
  const ageVals=ageKeys.map(k=>metric==='amount'?byAge[k].amt:metric==='qty'?byAge[k].qty:byAge[k].tx);
  dc('cust-age');charts['cust-age']=new Chart($('ch-cust-age').getContext('2d'),{
    type:'bar',
    data:{labels:ageKeys,datasets:[{label:metric==='amount'?'売上金額':metric==='qty'?'販売数量':'取引件数',data:ageVals,backgroundColor:CP_COLORS.slice(0,ageKeys.length),borderRadius:6,borderWidth:0}]},
    options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>metric==='amount'?fY(c.raw):fN(c.raw)}}}}
  });

  // ─ 顧客種別 × 曜日 ヒートマップ ─
  const validCustKeys=custKeys.filter(k=>k!=='未記録');
  if(validCustKeys.length>0){
    const allDowVals=validCustKeys.flatMap(k=>byCust[k].dows);
    const mxDow=Math.max(...allDowVals,1);
    let hmHtml=`<table style="border-collapse:separate;border-spacing:3px;font-size:12px;width:100%"><thead><tr><th style="padding:6px;color:var(--text2);text-align:left;font-size:11px;font-family:'Nunito',monospace">顧客種別</th>`;
    DOW.forEach(d=>hmHtml+=`<th style="padding:6px 10px;color:var(--text2);font-size:11px;font-family:'Nunito',monospace;text-align:center">${d}</th>`);
    hmHtml+='</tr></thead><tbody>';
    validCustKeys.forEach(k=>{
      hmHtml+=`<tr><td style="padding:6px;color:var(--text1);font-weight:800;font-size:12px;white-space:nowrap">${CUST_LABELS[k]||k}</td>`;
      byCust[k].dows.forEach(v=>{
        const r2=mxDow>0?v/mxDow:0;
        const bg=r2<.05?'var(--bg2)':`rgba(255,140,105,${.1+r2*.8})`;
        const tc=r2>.6?'#ffffff':'var(--text1)';
        hmHtml+=`<td style="padding:8px 10px;background:${bg};color:${tc};text-align:center;font-family:'Nunito',monospace;font-size:11px;font-weight:700;border-radius:6px">${v>0?(metric==='amount'?Math.round(v/1000)+'k':Math.round(v)):''}</td>`;
      });
      hmHtml+='</tr>';
    });
    hmHtml+='</tbody></table>';
    $('ct-dow-heatmap').innerHTML=hmHtml;
  }else{
    $('ct-dow-heatmap').innerHTML='<div style="color:var(--text3);font-size:13px;padding:12px;font-weight:700;">顧客種別データがありません</div>';
  }

  // ─ 顧客種別 × 時間帯 積み上げ棒グラフ ─
  dc('cust-hour');
  if(validCustKeys.length>0){
    charts['cust-hour']=new Chart($('ch-cust-hour').getContext('2d'),{
      type:'bar',
      data:{
        labels:[...Array(24).keys()].map(h=>h+'時'),
        datasets:validCustKeys.map((k,i)=>({
          label:CUST_LABELS[k]||k,
          data:byCust[k].hours,
          backgroundColor:CP_COLORS[i%CP_COLORS.length]+'dd',
          borderColor:CP_COLORS[i%CP_COLORS.length],
          borderWidth:1,
          borderRadius:4,
        }))
      },
      options:{...co(),scales:{...co().scales,x:{...co().scales.x,stacked:true},y:{...co().scales.y,stacked:true}},
        plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.dataset.label+': '+(metric==='amount'?fY(c.raw):fN(c.raw))}}}}
    });
  }

  // ─ 顧客種別別 人気商品 TOP5 表 ─
  if(validCustKeys.length>0){
    let tblHtml=`<table class="rtbl"><thead><tr><th>顧客種別</th>`;
    for(let i=1;i<=5;i++)tblHtml+=`<th>${i}位</th>`;
    tblHtml+='</tr></thead><tbody>';
    validCustKeys.forEach(k=>{
      const sorted=Object.entries(byCust[k].products).sort((a,b)=>b[1]-a[1]).slice(0,5);
      tblHtml+=`<tr><td style="font-weight:800;white-space:nowrap">${CUST_LABELS[k]||k}</td>`;
      for(let i=0;i<5;i++){
        if(sorted[i]){
          const pct=byCust[k].amt>0?sorted[i][1]/byCust[k].amt*100:0;
          tblHtml+=`<td style="font-size:12px"><div style="font-weight:700;color:var(--text0);">${sorted[i][0]}</div><div style="color:var(--text2);font-family:'Nunito',monospace;font-weight:700;">${fY(sorted[i][1])} (${fN(pct,1)}%)</div></td>`;
        }else tblHtml+='<td style="color:var(--text3)">─</td>';
      }
      tblHtml+='</tr>';
    });
    tblHtml+='</tbody></table>';
    $('ct-prod-table').innerHTML=tblHtml;
  }

  // ─ 客単価(顧客種別別)─
  const atvKeys=validCustKeys.filter(k=>byCust[k].tx>0);
  const atvVals=atvKeys.map(k=>byCust[k].amt/byCust[k].tx);
  dc('cust-atv');charts['cust-atv']=new Chart($('ch-cust-atv').getContext('2d'),{
    type:'bar',
    data:{labels:atvKeys.map(k=>CUST_LABELS[k]||k),datasets:[{label:'客単価',data:atvVals,backgroundColor:CP_COLORS.slice(0,atvKeys.length),borderRadius:6,borderWidth:0}]},
    options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>'客単価: '+fY(c.raw)}}}}
  });

  // ─ 割引利用状況 ─
  const discData=validCustKeys.map(k=>({key:k,rate:byCust[k].tx>0?byCust[k].discCnt/byCust[k].tx*100:0}));
  dc('cust-disc');charts['cust-disc']=new Chart($('ch-cust-disc').getContext('2d'),{
    type:'bar',
    data:{labels:discData.map(d=>CUST_LABELS[d.key]||d.key),datasets:[{label:'割引利用率',data:discData.map(d=>d.rate),backgroundColor:discData.map((_,i)=>CP_COLORS[i%CP_COLORS.length]+'dd'),borderColor:discData.map((_,i)=>CP_COLORS[i%CP_COLORS.length]),borderWidth:1,borderRadius:6}]},
    options:{...co(),plugins:{...CD.plugins,tooltip:{...CD.plugins.tooltip,callbacks:{label:c=>c.dataset.label+': '+fN(c.raw,1)+'%'}}},scales:{...co().scales,y:{...co().scales.y,max:100,ticks:{...co().scales.y.ticks,callback:v=>v+'%'}}}}
  });
}

// ─── キャンペーン効果 ─────────────────────────
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:12px;font-weight:700;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:24px;color:var(--text2);">→</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:12px;font-weight:700;color:var(--text2);margin-bottom:12px">キャンペーン後との比較: <span style="color:${vsPost>=0?'var(--green)':'var(--red)'}">${fP(vsPost)}</span>(反動確認)</div>`:''}
      ${lifts.length?`<div style="margin-bottom:12px"><div style="font-size:12px;font-weight:800;color:var(--text2);margin-bottom:6px">▶ 最も伸びた商品</div>${lifts.map((p,i)=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:13px;font-weight:700;"><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(255,140,105,.25)');g.addColorStop(1,'rgba(255,140,105,.01)');
  charts['cp']=new Chart(ctx,{type:'line',data:{labels:months,datasets:[
    {label:'月次売上',data:mAmt,borderColor:'#ff8c69',borderWidth:3,backgroundColor:g,fill:true,tension:.3,pointRadius:5,pointBackgroundColor:'#ff8c69'},
    ...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:4,backgroundColor:cp.color+'33',fill:true,tension:.3,pointRadius:6,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(255,140,105,.9)':r>.6?'rgba(107,203,139,.7)':'rgba(240,216,200,.8)';
      const col=r>.85?'#ffffff':'var(--text1)';
      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:'Nunito',monospace;font-size:10px;font-weight:800;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:8px 0;border-bottom:1px solid var(--border)">
      <div style="display:flex;justify-content:space-between;margin-bottom:6px">
        <span style="font-family:'Nunito',monospace;font-weight:900;font-size:14px;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:11px;font-weight:700;color:var(--text2);margin-top:4px">${x.h-1>=0?x.h-1+'時までに補充':' 開店前に補充'}推奨(平均需要の${fN(x.r*100,0)}%)</div>
    </div>`).join('')||'<div style="font-size:13px;font-weight:700;color:var(--text3);padding:10px;">目立ったリスク時間帯はありません</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);
    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:800">${p.n}</td>
    <td style="font-family:'Nunito',monospace;font-weight:700;">${fN(p.dayAvgQty,1)}個</td>
    <td><span style="font-weight:700">${DOW[p.pkDow]}曜</span> <span class="badge ba">×${fN(p.pkMult,1)}</span></td>
    <td style="font-family:'Nunito',monospace;font-size:16px;font-weight:900;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;font-weight:700;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(255,140,105,.9)':v<tmA*.88?'rgba(91,184,245,.6)':'rgba(240,216,200,.8)'),borderRadius:6,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.txid||(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)}の売上増になります。`});
  
  // 顧客種別アドバイス
  const hasCustData=rows.some(r=>r.customer_type&&r.customer_type!=='');
  if(hasCustData){
    const byCust2={};
    rows.forEach(r=>{const k=r.customer_type||'未記録';if(!byCust2[k])byCust2[k]={amt:0,tx:0};byCust2[k].amt+=r.amount;byCust2[k].tx++;});
    const custEntries=Object.entries(byCust2).filter(([k])=>k!=='未記録').sort((a,b)=>b[1].amt-a[1].amt);
    const CUST_LABELS2={general:'一般',member:'会員',student:'学生',senior:'シニア',staff:'スタッフ'};
    if(custEntries.length>=2){
      const topCust=custEntries[0];
      const topRatio=topCust[1].amt/totalAmt*100;
      advs.push({t:'teal',i:'👤',tl:`「${CUST_LABELS2[topCust[0]]||topCust[0]}」客が売上の${fN(topRatio,0)}%を占める主力`,d:`${custEntries.map(([k,v])=>`${CUST_LABELS2[k]||k}: ${fN(v.amt/totalAmt*100,0)}%`).join('・')}。`,a:`主力客層「${CUST_LABELS2[topCust[0]]||topCust[0]}」に合わせた商品展開・サービスを優先してください。`});
      // 割引利用率の高い客層
      const discRows=rows.filter(r=>r.discount_pct&&r.discount_pct>0);
      if(discRows.length>0){
        const discByCust={};
        discRows.forEach(r=>{const k=r.customer_type||'未記録';if(!discByCust[k])discByCust[k]={cnt:0,totalAmt:0};discByCust[k].cnt++;discByCust[k].totalAmt+=r.amount;});
        const discRate=rows.length>0?discRows.length/rows.length*100:0;
        advs.push({t:'info',i:'🏷️',tl:`割引適用率 ${fN(discRate,1)}%`,d:`${discRows.length}件の取引で割引が使われています。割引利用の多い客層に絞ったポイント制や特典検討も有効です。`,a:'割引の多い客層を「顧客分析」タブで確認し、ロイヤル顧客化を図りましょう。'});
      }
    }
  }
  
  // サマリー
  $('adv-summary').textContent=`期間: ${dates[0]}〜${dates[dates.length-1]}(${dates.length}日間) 総売上: ${fY(totalAmt)} 取引: ${txA.length}件 客単価: ${fY(avgTx)} 商品種類: ${ps.length}種`+(hasCustData?' 【顧客データあり】':'');
  
  // 描画
  $('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('MISE POS・Square・AirレジのCSVを直接ドロップできます','info');
</script>
</body>
</html>

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

ピックアップされています

便利なツール

  • 65本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
お店の売上分析ツール「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