"""
Build dashboard2.html from dashboard_data.json + User list.xlsx
Run: python build_html.py
"""
import json, openpyxl

# ── Load data ──────────────────────────────────────────────────
with open('dashboard_data.json', encoding='utf-8') as f:
    data = json.load(f)

wb = openpyxl.load_workbook('User list.xlsx', data_only=True)
ws = wb.active
user_map = {}
for i, row in enumerate(ws.iter_rows(values_only=True)):
    if i == 0: continue
    uid, email = row[0], row[1]
    if uid and email and uid not in user_map:
        user_map[int(uid)] = str(email).split('@')[0]

DATA_JS  = json.dumps(data, ensure_ascii=False, default=str)
NAMES_JS = json.dumps(user_map)

HTML = r"""<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Warehouse Efficiency Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
body{background:#0f172a;font-family:system-ui,sans-serif}
.card{background:#1e293b;border:1px solid #334155;border-radius:12px}
.section-title{font-size:15px;font-weight:600;color:#f1f5f9;margin-bottom:4px}
.section-sub{font-size:12px;color:#64748b;margin-bottom:16px}
select,input{background:#0f172a;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:4px 10px;font-size:13px}
.heat-cell{width:70px;height:36px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;border-radius:4px}
.rt-mode-btn{transition:background .15s}
</style>
</head>
<body class="p-4 md:p-6 min-h-screen text-slate-200">

<!-- HEADER -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
  <div>
    <h1 class="text-xl font-bold text-white">Warehouse Efficiency Dashboard</h1>
    <p class="text-slate-400 text-sm mt-0.5">Workload · Backlog · Thoi gian xu ly · Throughput</p>
  </div>
  <div class="flex flex-wrap gap-3 items-center">
    <div class="flex gap-2 items-center"><span class="text-slate-400 text-sm">Tu:</span><select id="filterFrom" onchange="applyFilter()"></select></div>
    <div class="flex gap-2 items-center"><span class="text-slate-400 text-sm">Den:</span><select id="filterTo" onchange="applyFilter()"></select></div>
    <button onclick="resetFilter()" class="bg-slate-700 hover:bg-slate-600 text-sm px-3 py-1.5 rounded-lg transition">Reset</button>
  </div>
</div>

<!-- KPI -->
<div id="kpiRow" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"></div>

<!-- PIPELINE BACKLOG -->
<div class="card p-5 mb-6">
  <div class="flex items-start justify-between mb-5">
    <div>
      <p class="section-title">Ton dong theo cong doan — Tinh theo Ma hang (tat ca ngay)</p>
      <p class="section-sub">So ma da qua cong doan truoc nhung chua hoan thanh cong doan nay</p>
    </div>
    <div id="backlogTotal" class="text-right"></div>
  </div>
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6" id="backlogCards"></div>
  <div class="space-y-4" id="backlogBars"></div>
</div>

<!-- CHART 1 -->
<div class="card p-5 mb-6">
  <div class="flex flex-wrap items-start justify-between gap-2 mb-4">
    <div>
      <p class="section-title">1. Workload theo ngay — So Ma & Control</p>
      <p class="section-sub">Cot dam = so ma (Part Number) | Cot mo = so Control (Packaging & Stock)</p>
    </div>
  </div>
  <div style="height:300px"><canvas id="chart1"></canvas></div>
</div>

<!-- CHART 2 -->
<div class="card p-5 mb-6">
  <p class="section-title">2. Workload theo User — tung cong doan</p>
  <p class="section-sub">Moi duong = 1 user · Y = so ma xu ly moi ngay</p>
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-2">
    <div><p class="text-xs font-semibold text-blue-400 mb-1">RECEIVING</p><div style="height:200px"><canvas id="chart2a"></canvas></div></div>
    <div><p class="text-xs font-semibold text-emerald-400 mb-1">QC IN</p><div style="height:200px"><canvas id="chart2b"></canvas></div></div>
    <div><p class="text-xs font-semibold text-violet-400 mb-1">PACKAGING IN</p><div style="height:200px"><canvas id="chart2c"></canvas></div></div>
    <div><p class="text-xs font-semibold text-amber-400 mb-1">STOCK IN</p><div style="height:200px"><canvas id="chart2d"></canvas></div></div>
  </div>
</div>

<!-- CHART 3 -->
<div class="card p-5 mb-6">
  <div class="flex items-start justify-between mb-4 gap-2">
    <div>
      <p class="section-title">3. Trung binh nang suat theo cong doan</p>
      <p class="section-sub">Y = TB so ma xu ly moi ngay · Diem thap nhat = Bottleneck</p>
    </div>
    <span id="bottleneckBadge" class="text-xs px-2 py-1 rounded-full bg-red-500/20 text-red-400 font-semibold whitespace-nowrap"></span>
  </div>
  <div style="height:260px"><canvas id="chart3"></canvas></div>
</div>

<!-- CHART 4: HEATMAP -->
<div class="card p-5 mb-6">
  <div class="flex flex-wrap items-center justify-between gap-3 mb-4">
    <div>
      <p class="section-title">4. Phan cong nhan su — Heatmap theo ngay</p>
      <p class="section-sub">Mau cang dam = cang nhieu ma · Packaging & Stock hien thi Ma/Control</p>
    </div>
    <select id="heatmapDate" onchange="renderHeatmap()"></select>
  </div>
  <div id="heatmapContainer" class="overflow-x-auto"></div>
</div>

<!-- CHART 5 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
  <div class="card p-5">
    <p class="section-title">5. Thoi gian cho TB giua cong doan</p>
    <p class="section-sub">Gio lam viec thuc te (tru gio nghi)</p>
    <div style="height:220px"><canvas id="chart5a"></canvas></div>
  </div>
  <div class="card p-5">
    <p class="section-title">5. Thoi gian xu ly tung ma (Receive to Stock)</p>
    <p class="section-sub">Moi diem = 1 control · X = ngay Receive · Y = tong gio lam viec</p>
    <div style="height:220px"><canvas id="chart5b"></canvas></div>
  </div>
</div>

<!-- PART SEARCH -->
<div class="card p-5 mb-6">
  <p class="section-title">5. Tra cuu timeline ma hang</p>
  <p class="section-sub">Nhap Part Number de xem ma do di qua cac cong doan khi nao</p>
  <div class="flex gap-3 mb-4">
    <input id="partSearch" type="text" placeholder="Nhap Part Number..." class="flex-1 max-w-sm" oninput="searchPart()"/>
    <span class="text-slate-500 text-sm self-center" id="searchHint"></span>
  </div>
  <div id="partTimeline" class="overflow-x-auto"></div>
</div>

<!-- CHART 6: THROUGHPUT -->
<div class="card p-5 mb-6">
  <p class="section-title">6. Ty le throughput — Cohort theo ngay Receive</p>
  <p class="section-sub">Trong so ma nhan ngay do, bao nhieu % da hoan thanh tung cong doan (luon <= 100%)</p>
  <div style="height:260px"><canvas id="chart6"></canvas></div>
</div>

<!-- CHART 7: RECEIVE TIME -->
<div class="card p-5 mb-6">
  <div class="flex flex-wrap items-start justify-between gap-3 mb-4">
    <div>
      <p class="section-title">7. Phan phoi thoi gian nhan hang (Receiving ID)</p>
      <p class="section-sub">Moi cot = so chuyen giao hang trong khung gio · Xem theo ngay / thang / nam</p>
    </div>
    <div class="flex flex-wrap gap-2 items-center">
      <div class="flex rounded-lg overflow-hidden border border-slate-600">
        <button id="btnDay"   onclick="setRTMode('day')"   class="rt-mode-btn px-3 py-1.5 text-sm bg-blue-600 text-white">Ngay</button>
        <button id="btnMonth" onclick="setRTMode('month')" class="rt-mode-btn px-3 py-1.5 text-sm bg-slate-700 text-slate-300">Thang</button>
        <button id="btnYear"  onclick="setRTMode('year')"  class="rt-mode-btn px-3 py-1.5 text-sm bg-slate-700 text-slate-300">Nam</button>
      </div>
      <select id="rtPeriodSelect" onchange="renderChart7()" class="min-w-32"></select>
    </div>
  </div>
  <div class="grid grid-cols-4 gap-3 mb-5" id="zoneCards"></div>
  <div style="height:260px"><canvas id="chart7"></canvas></div>
</div>

<!-- COMPLETION SNAPSHOT -->
<div class="card p-5 mb-6">
  <div class="flex flex-wrap items-start justify-between gap-3 mb-4">
    <div>
      <p class="section-title">Ty le hoan thanh cong viec nhap hang</p>
      <p class="section-sub">Lu ke den ngay duoc chon · Chon Ngay / Thang / Nam + ky cu the</p>
    </div>
    <div class="flex flex-wrap gap-2 items-center">
      <div class="flex rounded-lg overflow-hidden border border-slate-600">
        <button id="csBtnDay"   onclick="setCSMode('day')"   class="cs-mode-btn px-3 py-1.5 text-sm bg-blue-600 text-white">Ngay</button>
        <button id="csBtnMonth" onclick="setCSMode('month')" class="cs-mode-btn px-3 py-1.5 text-sm bg-slate-700 text-slate-300">Thang</button>
        <button id="csBtnYear"  onclick="setCSMode('year')"  class="cs-mode-btn px-3 py-1.5 text-sm bg-slate-700 text-slate-300">Nam</button>
      </div>
      <select id="csPeriodSelect" onchange="renderCompletion()" class="min-w-32"></select>
    </div>
  </div>
  <!-- Table -->
  <div class="overflow-x-auto">
    <table class="w-full text-sm border-collapse">
      <thead>
        <tr class="border-b border-slate-700">
          <th class="text-left py-2 pr-4 text-slate-400 font-medium">Hang muc</th>
          <th class="text-right py-2 px-3 text-slate-400 font-medium">Can xu ly</th>
          <th class="text-right py-2 px-3 text-slate-400 font-medium">Hoan thanh</th>
          <th class="text-right py-2 px-3 text-slate-400 font-medium">Con lai</th>
          <th class="text-right py-2 px-3 text-slate-400 font-medium">Ty le</th>
          <th class="py-2 px-3 text-slate-400 font-medium">Tien do</th>
        </tr>
      </thead>
      <tbody id="completionTableBody" class="divide-y divide-slate-800"></tbody>
    </table>
  </div>
</div>

<!-- SUMMARY TABLE -->
<div class="card p-5">
  <p class="section-title">Bang tong hop</p>
  <div class="overflow-x-auto mt-3">
    <table class="w-full text-sm">
      <thead><tr id="summaryHead" class="border-b border-slate-700 text-slate-400"></tr></thead>
      <tbody id="summaryBody" class="divide-y divide-slate-800"></tbody>
    </table>
  </div>
</div>

<script>
const RAW        = ###DATA###;
const USER_NAMES = ###NAMES###;

const STAGE_NAMES  = ['Receive','QC In','Packaging In','Stock In'];
const STAGE_COLORS = {
  'Receive':      {solid:'rgba(59,130,246,0.85)',  light:'rgba(59,130,246,0.25)',  hex:'#3b82f6'},
  'QC In':        {solid:'rgba(16,185,129,0.85)',  light:'rgba(16,185,129,0.25)',  hex:'#10b981'},
  'Packaging In': {solid:'rgba(139,92,246,0.85)',  light:'rgba(139,92,246,0.25)',  hex:'#8b5cf6'},
  'Stock In':     {solid:'rgba(245,158,11,0.85)',  light:'rgba(245,158,11,0.25)',  hex:'#f59e0b'},
};
const NS_TARGETS = {'Receive':300,'QC In':200,'Packaging In':350,'Stock In':750};
const USER_PALETTE = ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4','#ec4899','#84cc16','#f97316','#6366f1'];
const GRID = 'rgba(148,163,184,0.07)';

Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = GRID;
Chart.defaults.font.family = 'system-ui,sans-serif';

let filteredDates = [...RAW.dates];
let chart1Inst, chart2Insts={a:null,b:null,c:null,d:null}, chart3Inst, chart5aInst, chart5bInst, chart6Inst, chart7Inst;
let rtMode = 'day';

function fmtLabel(d) { const [y,m,day]=d.split('-'); return day+'/'+m; }

// ── Filters ──────────────────────────────────────────────────
function initFilters() {
  const from=document.getElementById('filterFrom'), to=document.getElementById('filterTo');
  const hd=document.getElementById('heatmapDate');
  RAW.dates.forEach(d => {
    const o='<option value="'+d+'">'+fmtLabel(d)+'</option>';
    from.innerHTML+=o; to.innerHTML+=o; hd.innerHTML+=o;
  });
  to.selectedIndex=RAW.dates.length-1;
  hd.selectedIndex=RAW.dates.length-1;
}
function applyFilter() {
  const from=document.getElementById('filterFrom').value, to=document.getElementById('filterTo').value;
  filteredDates=RAW.dates.filter(d=>d>=from&&d<=to);
  renderAll();
}
function resetFilter() {
  document.getElementById('filterFrom').selectedIndex=0;
  document.getElementById('filterTo').selectedIndex=RAW.dates.length-1;
  filteredDates=[...RAW.dates]; renderAll();
}

// ── KPI ──────────────────────────────────────────────────────
function renderKPI() {
  const colors=['blue','emerald','violet','amber'];
  document.getElementById('kpiRow').innerHTML=STAGE_NAMES.map((stage,si)=>{
    const c=colors[si], t=NS_TARGETS[stage];
    const total=filteredDates.reduce((s,d)=>s+(RAW.stage_workload[stage]?.[d]?.parts||0),0);
    const ctrls=filteredDates.reduce((s,d)=>s+(RAW.stage_workload[stage]?.[d]?.controls||0),0);
    const avg=filteredDates.length?Math.round(total/filteredDates.length):0;
    const pct=Math.min(100,Math.round(avg/t*100));
    const pc=pct>=100?'text-emerald-400':pct>=70?'text-amber-400':'text-red-400';
    const showCtrl=stage==='Packaging In'||stage==='Stock In';
    return '<div class="card p-4">'
      +'<div class="flex items-center justify-between mb-1"><span class="text-slate-400 text-xs uppercase tracking-wide">'+stage+'</span>'
      +'<span class="text-'+c+'-400 text-xs font-semibold">Target '+t+'/ngay</span></div>'
      +'<p class="text-2xl font-bold text-'+c+'-400">'+total.toLocaleString()+'</p>'
      +'<p class="text-slate-500 text-xs">'+(showCtrl?ctrls.toLocaleString()+' controls · ':'')+'tong ma</p>'
      +'<div class="mt-2 h-1.5 bg-slate-700 rounded-full overflow-hidden"><div class="h-full bg-'+c+'-500 rounded-full" style="width:'+pct+'%"></div></div>'
      +'<p class="text-slate-500 text-xs mt-1"><span class="'+pc+'">'+pct+'%</span> vs target · TB '+avg+'/ngay</p>'
      +'</div>';
  }).join('');
}

// ── Backlog ───────────────────────────────────────────────────
function renderBacklog() {
  const b = RAW.pipeline_backlog;
  document.getElementById('backlogTotal').innerHTML =
    '<p class="text-slate-400 text-xs">Tong ma hang</p>'
    +'<p class="text-2xl font-bold text-white">'+b.received+'</p>'
    +'<p class="text-emerald-400 text-xs mt-0.5">'+b.pct_stock+'% da vao kho</p>';

  const stages=[
    {label:'Receiving',    done:b.received,   pending:null,           pct:100,       color:'#3b82f6'},
    {label:'QC In',        done:b.qc_done,    pending:b.pending_qc,   pct:b.pct_qc,  color:'#10b981'},
    {label:'Packaging In', done:b.pack_done,  pending:b.pending_pack, pct:b.pct_pack,color:'#8b5cf6'},
    {label:'Stock In',     done:b.stock_done, pending:b.pending_stock,pct:b.pct_stock,color:'#f59e0b'},
  ];

  function pc(n){return n===null?'#64748b':n>50?'#ef4444':n>10?'#f59e0b':'#10b981';}

  document.getElementById('backlogCards').innerHTML=stages.map(s=>{
    const pendLine = s.pending===null
      ? '<p class="text-slate-500 text-xs mt-1">Khong do duoc ton dong</p>'
      : s.pending===0
        ? '<p class="text-emerald-400 text-xs mt-1 font-semibold">Khong ton dong</p>'
        : '<p class="text-xs mt-1 font-semibold" style="color:'+pc(s.pending)+'">'+s.pending+' ma dang cho</p>';
    return '<div class="rounded-xl p-4" style="background:#1e293b;border:1px solid #334155;border-left:3px solid '+s.color+'">'
      +'<p class="text-xs text-slate-400 uppercase tracking-wide mb-2">'+s.label+'</p>'
      +'<p class="text-2xl font-bold" style="color:'+s.color+'">'+s.done+'</p>'
      +'<p class="text-slate-500 text-xs">ma xong</p>'+pendLine+'</div>';
  }).join('');

  document.getElementById('backlogBars').innerHTML=stages.slice(1).map(s=>{
    const pp=Math.max(0,100-s.pct), color=pc(s.pending);
    return '<div>'
      +'<div class="flex justify-between items-center mb-1.5">'
      +'<span class="text-sm font-semibold" style="color:'+s.color+'">'+s.label+'</span>'
      +'<span class="text-xs text-slate-400">'+s.done+' / '+b.received+' ma'
      +(s.pending>0?' &middot; <span style="color:'+color+';font-weight:600">'+s.pending+' dang cho</span>':' &middot; <span class="text-emerald-400 font-semibold">Sach</span>')
      +'</span></div>'
      +'<div class="flex h-6 bg-slate-700 rounded-full overflow-hidden">'
      +'<div style="width:'+s.pct+'%;background:'+s.color+'" class="h-full flex items-center justify-end pr-2">'
      +(s.pct>15?'<span class="text-white text-xs font-bold">'+s.pct+'%</span>':'')
      +'</div>'
      +(pp>0?'<div style="width:'+pp+'%;background:'+color+'33;border-left:2px dashed '+color+'" class="h-full flex items-center pl-1.5">'
        +(pp>5?'<span class="text-xs font-semibold" style="color:'+color+'">'+pp.toFixed(1)+'%</span>':'')+'</div>':'')
      +'</div></div>';
  }).join('');
}

// ── Chart 1 ──────────────────────────────────────────────────
function renderChart1() {
  const labels=filteredDates.map(fmtLabel);
  const ds=[];
  STAGE_NAMES.forEach(stage=>{
    const col=STAGE_COLORS[stage];
    ds.push({label:stage,data:filteredDates.map(d=>RAW.stage_workload[stage]?.[d]?.parts||0),
      backgroundColor:col.solid,borderColor:col.hex,borderWidth:1,borderRadius:3});
    if(stage==='Packaging In'||stage==='Stock In')
      ds.push({label:stage+' (ctrl)',data:filteredDates.map(d=>RAW.stage_workload[stage]?.[d]?.controls||0),
        backgroundColor:col.light,borderColor:col.hex,borderWidth:1,borderRadius:3});
  });
  if(chart1Inst)chart1Inst.destroy();
  chart1Inst=new Chart(document.getElementById('chart1').getContext('2d'),{
    type:'bar',data:{labels,datasets:ds},
    options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
      plugins:{legend:{labels:{boxWidth:10,font:{size:10},color:'#94a3b8'}}},
      scales:{x:{grid:{color:GRID}},y:{grid:{color:GRID},beginAtZero:true}}}
  });
}

// ── Chart 2 ──────────────────────────────────────────────────
function makeUserLineChart(canvasId,stage,key){
  const labels=filteredDates.map(fmtLabel);
  const users=RAW.stage_users[stage];
  const ds=users.map((u,i)=>({
    label:USER_NAMES[parseInt(u)]||'User '+u,
    data:filteredDates.map(d=>RAW.stage_workload[stage]?.[d]?.users?.[u]||0),
    borderColor:USER_PALETTE[i%USER_PALETTE.length],
    backgroundColor:USER_PALETTE[i%USER_PALETTE.length]+'33',
    borderWidth:2,pointRadius:4,tension:0.3,fill:false,
  }));
  if(chart2Insts[key]){chart2Insts[key].destroy();chart2Insts[key]=null;}
  chart2Insts[key]=new Chart(document.getElementById(canvasId).getContext('2d'),{
    type:'line',data:{labels,datasets:ds},
    options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
      plugins:{legend:{labels:{boxWidth:10,font:{size:10},color:'#94a3b8'}}},
      scales:{x:{grid:{color:GRID},ticks:{font:{size:10}}},y:{grid:{color:GRID},beginAtZero:true}}}
  });
}
function renderChart2(){
  makeUserLineChart('chart2a','Receive','a');
  makeUserLineChart('chart2b','QC In','b');
  makeUserLineChart('chart2c','Packaging In','c');
  makeUserLineChart('chart2d','Stock In','d');
}

// ── Chart 3 ──────────────────────────────────────────────────
function renderChart3(){
  const avgs=STAGE_NAMES.map(stage=>{
    const vals=filteredDates.map(d=>RAW.stage_workload[stage]?.[d]?.parts||0).filter(v=>v>0);
    return vals.length?Math.round(vals.reduce((a,b)=>a+b,0)/vals.length):0;
  });
  const minV=Math.min(...avgs), bi=avgs.indexOf(minV);
  document.getElementById('bottleneckBadge').textContent='Bottleneck: '+STAGE_NAMES[bi]+' (TB '+minV+' ma/ngay)';
  if(chart3Inst)chart3Inst.destroy();
  chart3Inst=new Chart(document.getElementById('chart3').getContext('2d'),{
    type:'line',
    data:{labels:STAGE_NAMES,datasets:[{label:'TB ma/ngay',data:avgs,
      borderColor:'#6366f1',backgroundColor:'rgba(99,102,241,0.18)',borderWidth:2.5,fill:true,tension:0.3,
      pointRadius:avgs.map((_,i)=>i===bi?8:5),
      pointBackgroundColor:avgs.map((_,i)=>i===bi?'#ef4444':'#6366f1'),
      pointBorderColor:avgs.map((_,i)=>i===bi?'#ef4444':'#6366f1'),
    }]},
    options:{responsive:true,maintainAspectRatio:false,
      plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>'TB: '+ctx.raw+' ma/ngay'+(ctx.dataIndex===bi?' <- BOTTLENECK':'')}}},
      scales:{x:{grid:{color:GRID}},y:{grid:{color:GRID},beginAtZero:true}}}
  });
}

// ── Chart 4: Heatmap ─────────────────────────────────────────
function renderHeatmap(){
  const sel=document.getElementById('heatmapDate').value;
  const container=document.getElementById('heatmapContainer');
  const allUsers=new Set();
  STAGE_NAMES.forEach(stage=>Object.keys(RAW.stage_workload[stage]?.[sel]?.users||{}).forEach(u=>allUsers.add(u)));
  const userList=[...allUsers].sort((a,b)=>parseInt(a)-parseInt(b));
  if(!userList.length){container.innerHTML='<p class="text-slate-500 text-sm">Khong co du lieu.</p>';return;}
  let maxVal=1;
  userList.forEach(u=>STAGE_NAMES.forEach(stage=>{const v=RAW.stage_workload[stage]?.[sel]?.users?.[u]||0;if(v>maxVal)maxVal=v;}));
  function heatColor(v){
    if(!v)return'background:#1e293b;color:#475569';
    const p=v/maxVal,r=Math.round(30+p*220),g=Math.round(60-p*20),b=Math.round(180-p*160);
    return'background:rgb('+r+','+g+','+b+');color:'+(p>0.5?'#fff':'#e2e8f0');
  }
  const CTRL_STAGES=['Packaging In','Stock In'];
  let h='<table class="border-collapse text-xs"><thead><tr>'
    +'<th class="text-slate-400 pr-4 pb-2 text-left font-normal">User</th>'
    +STAGE_NAMES.map(s=>'<th class="pb-2 px-1 text-center font-semibold" style="color:'+STAGE_COLORS[s].hex+';min-width:90px">'+s+'</th>').join('')
    +'<th class="pb-2 px-1 text-center text-slate-400 font-normal">Total</th>'
    +'</tr></thead><tbody>';
  userList.forEach(u=>{
    const vals=STAGE_NAMES.map(stage=>RAW.stage_workload[stage]?.[sel]?.users?.[u]||0);
    const total=vals.reduce((a,b)=>a+b,0);
    h+='<tr><td class="pr-4 py-1 text-slate-300 font-semibold">'+(USER_NAMES[parseInt(u)]||'User '+u)+'</td>'
      +STAGE_NAMES.map((stage,si)=>{
        const parts=RAW.stage_workload[stage]?.[sel]?.users?.[u]||0;
        const ctrls=CTRL_STAGES.includes(stage)?(RAW.stage_workload[stage]?.[sel]?.user_controls?.[u]||0):null;
        const label=ctrls!==null&&parts?(parts+'/<span style="opacity:.7">'+ctrls+'</span>'):( parts||'');
        return'<td class="px-1 py-1"><div class="heat-cell mx-auto" style="'+heatColor(parts)+'">'+label+'</div></td>';
      }).join('')
      +'<td class="px-1 py-1 text-center text-slate-200 font-bold">'+total+'</td></tr>';
  });
  const stageTotals=STAGE_NAMES.map(stage=>Object.values(RAW.stage_workload[stage]?.[sel]?.users||{}).reduce((a,b)=>a+b,0));
  const stageCtrl=STAGE_NAMES.map(stage=>CTRL_STAGES.includes(stage)?(RAW.stage_workload[stage]?.[sel]?.controls||0):null);
  h+='<tr class="border-t border-slate-700"><td class="pr-4 py-2 text-slate-400 font-semibold text-xs uppercase">Total</td>'
    +STAGE_NAMES.map((_,i)=>{const t=stageTotals[i],c=stageCtrl[i];
      return'<td class="px-1 py-2 text-center text-white font-bold">'+(c!==null?(t+'/<span style="opacity:.7">'+c+'</span>'):t)+'</td>';
    }).join('')
    +'<td class="px-1 py-2 text-center text-white font-bold">'+stageTotals.reduce((a,b)=>a+b,0)+'</td></tr>';
  container.innerHTML=h+'</tbody></table>';
}

// ── Chart 5a ─────────────────────────────────────────────────
function renderChart5a(){
  const labels=Object.keys(RAW.avg_wait), vals=Object.values(RAW.avg_wait);
  if(chart5aInst)chart5aInst.destroy();
  chart5aInst=new Chart(document.getElementById('chart5a').getContext('2d'),{
    type:'bar',data:{labels,datasets:[{label:'Gio lam viec',data:vals,
      backgroundColor:['#3b82f6','#10b981','#8b5cf6','#f59e0b'],borderRadius:5}]},
    options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,
      plugins:{legend:{display:false}},
      scales:{x:{grid:{color:GRID},ticks:{callback:v=>v+'h'}},y:{grid:{color:GRID}}}}
  });
}

// ── Chart 5b ─────────────────────────────────────────────────
function renderChart5b(){
  const inRange=RAW.item_timelines.filter(x=>x.wh_total!==null&&filteredDates.includes(x.receive_date));
  function bucket(h){return h<=4?'<=4h':h<=16?'4-16h':h<=48?'16-48h':'>48h';}
  const bColors={'<=4h':'rgba(16,185,129,0.7)','4-16h':'rgba(59,130,246,0.7)','16-48h':'rgba(245,158,11,0.7)','>48h':'rgba(239,68,68,0.7)'};
  const grouped={};
  inRange.forEach(x=>{const b=bucket(x.wh_total);if(!grouped[b])grouped[b]=[];grouped[b].push({x:x.receive_date,y:x.wh_total});});
  const ds=Object.entries(grouped).map(([b,pts])=>({label:b,data:pts,backgroundColor:bColors[b]||'#64748b',pointRadius:4}));
  if(chart5bInst)chart5bInst.destroy();
  chart5bInst=new Chart(document.getElementById('chart5b').getContext('2d'),{
    type:'scatter',data:{datasets:ds},
    options:{responsive:true,maintainAspectRatio:false,
      plugins:{legend:{labels:{boxWidth:10,font:{size:10},color:'#94a3b8'}},
        tooltip:{callbacks:{label:ctx=>ctx.raw.x+': '+ctx.raw.y+'h'}}},
      scales:{x:{type:'category',labels:filteredDates,ticks:{callback:(v,i)=>fmtLabel(filteredDates[i]||'')},grid:{color:GRID}},
        y:{grid:{color:GRID},ticks:{callback:v=>v+'h'},beginAtZero:true}}}
  });
}

// ── Part Search ───────────────────────────────────────────────
function searchPart(){
  const q=document.getElementById('partSearch').value.trim().toLowerCase();
  const container=document.getElementById('partTimeline'), hint=document.getElementById('searchHint');
  if(!q||q.length<2){container.innerHTML='';hint.textContent='';return;}
  const items=RAW.item_timelines.filter(x=>x.part&&x.part.toLowerCase().includes(q));
  hint.textContent=items.length+' ket qua';
  if(!items.length){container.innerHTML='<p class="text-slate-500 text-sm">Khong tim thay.</p>';return;}
  const stageKeys=[{key:'receive',label:'Receive',color:'#3b82f6'},{key:'qc',label:'QC',color:'#10b981'},{key:'pack',label:'Pack',color:'#8b5cf6'},{key:'stock',label:'Stock',color:'#f59e0b'}];
  function shortDt(iso){if(!iso)return'--';const d=new Date(iso);return d.getDate().toString().padStart(2,'0')+'/'+(d.getMonth()+1).toString().padStart(2,'0')+' '+d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0');}
  let h='<table class="w-full text-xs border-collapse"><thead><tr class="border-b border-slate-700 text-slate-400">'
    +'<th class="text-left pb-2 pr-4">Part Number</th><th class="text-left pb-2 pr-4">Control</th>'
    +stageKeys.map(s=>'<th class="text-center pb-2 px-2" style="color:'+s.color+'">'+s.label+'</th>').join('')
    +'<th class="text-center pb-2 px-2 text-slate-400">Tong gio</th></tr></thead><tbody class="divide-y divide-slate-800">';
  items.slice(0,30).forEach(x=>{
    h+='<tr class="hover:bg-slate-800/40"><td class="py-1.5 pr-4 text-slate-200 font-medium">'+x.part+'</td><td class="py-1.5 pr-4 text-slate-400">'+x.ctrl+'</td>'
      +stageKeys.map(s=>'<td class="py-1.5 px-2 text-center" style="color:'+s.color+'">'+shortDt(x[s.key])+'</td>').join('')
      +'<td class="py-1.5 px-2 text-center font-bold '+(x.wh_total>24?'text-red-400':x.wh_total>8?'text-amber-400':'text-emerald-400')+'">'+(x.wh_total!==null?x.wh_total+'h':'--')+'</td></tr>';
  });
  h+='</tbody></table>'+(items.length>30?'<p class="text-slate-500 text-xs mt-2">Hien thi 30/'+items.length+' ket qua dau.</p>':'');
  container.innerHTML=h;
}

// ── Chart 6 ──────────────────────────────────────────────────
function renderChart6(){
  const labels=filteredDates.map(fmtLabel);
  const keys=[
    {key:'r_q',  label:'% cohort da QC',      color:'#3b82f6'},
    {key:'q_p',  label:'% cohort da Packaging',color:'#10b981'},
    {key:'p_s',  label:'% cohort da Stock In', color:'#8b5cf6'},
    {key:'total',label:'% cohort hoan thanh',  color:'#f59e0b'},
  ];
  const ds=keys.map(k=>({label:k.label,data:filteredDates.map(d=>RAW.throughput[d]?.[k.key]??null),
    borderColor:k.color,backgroundColor:k.color+'22',borderWidth:2,pointRadius:4,tension:0.3,fill:false,spanGaps:true}));
  ds.push({label:'100%',data:filteredDates.map(()=>100),borderColor:'rgba(148,163,184,0.3)',borderWidth:1.5,borderDash:[5,5],pointRadius:0,fill:false});
  if(chart6Inst)chart6Inst.destroy();
  chart6Inst=new Chart(document.getElementById('chart6').getContext('2d'),{
    type:'line',data:{labels,datasets:ds},
    options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
      plugins:{legend:{labels:{boxWidth:10,font:{size:10},color:'#94a3b8'}}},
      scales:{x:{grid:{color:GRID}},y:{grid:{color:GRID},ticks:{callback:v=>v+'%'},beginAtZero:true,max:110}}}
  });
}

// ── Chart 7 ──────────────────────────────────────────────────
const ZONE_CFG=[
  {key:'morning',  label:'Sang (8-11h)',   color:'#3b82f6'},
  {key:'midday',   label:'Trua (11-14h)',  color:'#10b981'},
  {key:'afternoon',label:'Chieu (14-17h)', color:'#f59e0b'},
  {key:'evening',  label:'Toi (17h+)',     color:'#ef4444'},
];
function setRTMode(mode){
  rtMode=mode;
  ['Day','Month','Year'].forEach(m=>{
    const b=document.getElementById('btn'+m);
    if(b)b.className='rt-mode-btn px-3 py-1.5 text-sm '+(mode===m.toLowerCase()?'bg-blue-600 text-white':'bg-slate-700 text-slate-300');
  });
  buildRTPeriodOptions(); renderChart7();
}
function buildRTPeriodOptions(){
  const sel=document.getElementById('rtPeriodSelect');
  const dk=rtMode==='day'?'by_day':rtMode==='month'?'by_month':'by_year';
  const keys=Object.keys(RAW.receive_time_dist[dk]).sort().reverse();
  sel.innerHTML=keys.map(k=>{
    let label=k;
    if(rtMode==='day'){const[y,m,d]=k.split('-');label=d+'/'+m+'/'+y;}
    else if(rtMode==='month'){const[y,m]=k.split('-');label='Thang '+m+'/'+y;}
    else label='Nam '+k;
    return'<option value="'+k+'">'+label+'</option>';
  }).join('');
}
function renderZoneCards(zones,total){
  document.getElementById('zoneCards').innerHTML=ZONE_CFG.map(z=>{
    const cnt=zones[z.key]||0, pct=total?Math.round(cnt/total*100):0, warn=z.key==='evening'&&cnt>0;
    return'<div class="rounded-xl p-4 border border-slate-700" style="background:'+z.color+'18">'
      +'<p class="text-xs mb-1" style="color:'+z.color+'">'+z.label+'</p>'
      +'<p class="text-2xl font-bold" style="color:'+z.color+'">'+cnt+'</p>'
      +'<p class="text-xs mt-1 '+(warn?'font-semibold':'text-slate-500')+'" style="'+(warn?'color:#f87171':'')+'">'+pct+'% tong chuyen'+(warn?' (!)':'')+'</p>'
      +'</div>';
  }).join('');
}
function renderChart7(){
  const dk=rtMode==='day'?'by_day':rtMode==='month'?'by_month':'by_year';
  const period=document.getElementById('rtPeriodSelect').value;
  const entry=RAW.receive_time_dist[dk]?.[period];
  if(!entry)return;
  renderZoneCards(entry.zones,entry.total);
  const hours=Array.from({length:12},(_,i)=>(i+8)+':00');
  const hourKeys=Array.from({length:12},(_,i)=>String(i+8));
  function barColor(h){const hi=parseInt(h);return hi>=17?'rgba(239,68,68,0.82)':hi>=14?'rgba(245,158,11,0.82)':hi>=11?'rgba(16,185,129,0.82)':'rgba(59,130,246,0.82)';}
  if(chart7Inst){chart7Inst.destroy();chart7Inst=null;}
  chart7Inst=new Chart(document.getElementById('chart7').getContext('2d'),{
    type:'bar',
    data:{labels:hours,datasets:[{label:'So chuyen',data:hourKeys.map(h=>entry.hours[h]||0),backgroundColor:hourKeys.map(h=>barColor(h)),borderRadius:6,borderWidth:0}]},
    options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},
      scales:{x:{grid:{color:GRID}},y:{grid:{color:GRID},beginAtZero:true,ticks:{stepSize:1}}}},
    plugins:[{id:'eveningZone',afterDraw(chart){
      const{ctx,chartArea,scales}=chart; if(!chartArea||!scales.x)return;
      const bw=(chartArea.right-chartArea.left)/12, x17=chartArea.left+bw*9;
      ctx.save(); ctx.fillStyle='rgba(239,68,68,0.06)';
      ctx.fillRect(x17,chartArea.top,chartArea.right-x17,chartArea.bottom-chartArea.top);
      ctx.setLineDash([4,3]); ctx.strokeStyle='rgba(239,68,68,0.5)'; ctx.lineWidth=1.5;
      ctx.beginPath(); ctx.moveTo(x17,chartArea.top); ctx.lineTo(x17,chartArea.bottom); ctx.stroke(); ctx.restore();
    }}]
  });
}

// ── Summary Table ─────────────────────────────────────────────
function renderSummaryTable(){
  document.getElementById('summaryHead').innerHTML='<th class="text-left pb-3 pr-4">Ngay</th>'
    +'<th class="text-right pb-3 px-3 text-blue-400">Recv Ma</th>'
    +'<th class="text-right pb-3 px-3 text-emerald-400">QC Ma</th>'
    +'<th class="text-right pb-3 px-3 text-violet-400">Pkg Ma</th>'
    +'<th class="text-right pb-3 px-3 text-violet-300">Pkg Ctrl</th>'
    +'<th class="text-right pb-3 px-3 text-amber-400">Stock Ma</th>'
    +'<th class="text-right pb-3 px-3 text-amber-300">Stock Ctrl</th>'
    +'<th class="text-right pb-3 px-3 text-slate-400">% Hoan thanh</th>';
  const sw=RAW.stage_workload;
  document.getElementById('summaryBody').innerHTML=filteredDates.map(d=>{
    const tp=RAW.throughput[d]||{};
    const pc=tp.total>=80?'text-emerald-400':tp.total>=50?'text-amber-400':'text-red-400';
    return'<tr class="hover:bg-slate-800/40 transition-colors">'
      +'<td class="py-2 pr-4 text-white font-medium">'+fmtLabel(d)+'</td>'
      +'<td class="py-2 px-3 text-right">'+(sw['Receive']?.[d]?.parts||'--')+'</td>'
      +'<td class="py-2 px-3 text-right">'+(sw['QC In']?.[d]?.parts||'--')+'</td>'
      +'<td class="py-2 px-3 text-right">'+(sw['Packaging In']?.[d]?.parts||'--')+'</td>'
      +'<td class="py-2 px-3 text-right text-slate-400">'+(sw['Packaging In']?.[d]?.controls||'--')+'</td>'
      +'<td class="py-2 px-3 text-right">'+(sw['Stock In']?.[d]?.parts||'--')+'</td>'
      +'<td class="py-2 px-3 text-right text-slate-400">'+(sw['Stock In']?.[d]?.controls||'--')+'</td>'
      +'<td class="py-2 px-3 text-right font-semibold '+pc+'">'+(tp.total?tp.total+'%':'--')+'</td>'
      +'</tr>';
  }).join('');
}

// ── Completion Snapshot ───────────────────────────────────────
let csMode = 'day';
function setCSMode(mode) {
  csMode = mode;
  ['Day','Month','Year'].forEach(m => {
    const b = document.getElementById('csBtn'+m);
    if(b) b.className = 'cs-mode-btn px-3 py-1.5 text-sm '+(mode===m.toLowerCase()?'bg-blue-600 text-white':'bg-slate-700 text-slate-300');
  });
  buildCSPeriodOptions();
  renderCompletion();
}
function buildCSPeriodOptions() {
  const sel = document.getElementById('csPeriodSelect');
  const dk  = csMode==='day'?'by_day':csMode==='month'?'by_month':'by_year';
  const keys = Object.keys(RAW.completion_snapshots[dk]).sort().reverse();
  sel.innerHTML = keys.map(k => {
    let label = k;
    if(csMode==='day'){const[y,m,d]=k.split('-');label=d+'/'+m+'/'+y;}
    else if(csMode==='month'){const[y,m]=k.split('-');label='Thang '+m+'/'+y;}
    else label='Nam '+k;
    return '<option value="'+k+'">'+label+'</option>';
  }).join('');
}
function renderCompletion() {
  const dk  = csMode==='day'?'by_day':csMode==='month'?'by_month':'by_year';
  const period = document.getElementById('csPeriodSelect').value;
  const s = RAW.completion_snapshots[dk]?.[period];
  if(!s){document.getElementById('completionTableBody').innerHTML='<tr><td colspan="6" class="py-4 text-slate-500 text-center">Khong co du lieu.</td></tr>';return;}

  function pctColor(p){return p>=90?'text-emerald-400':p>=60?'text-amber-400':'text-red-400';}
  function bar(pct,color){
    const c=pct>=90?'#10b981':pct>=60?'#f59e0b':'#ef4444';
    return '<div class="w-32 h-4 bg-slate-700 rounded-full overflow-hidden inline-block align-middle">'
      +'<div style="width:'+pct+'%;background:'+c+'" class="h-full rounded-full"></div></div>'
      +' <span class="text-xs ml-1 '+pctColor(pct)+'">'+pct+'%</span>';
  }

  const rows = [
    {label:'Don hang nhan (Packing ID)', total:s.packing_total, done:s.packing_done, pct:s.packing_pct, color:'#64748b'},
    {label:'Receiving (so ma)',           total:s.recv_total,    done:s.recv_total,   pct:100,           color:'#3b82f6'},
    {label:'QC In',                       total:s.qc_total,      done:s.qc_done,      pct:s.qc_pct,      color:'#10b981'},
    {label:'Packaging In',                total:s.pack_total,    done:s.pack_done,    pct:s.pack_pct,    color:'#8b5cf6'},
    {label:'Stock In',                    total:s.stock_total,   done:s.stock_done,   pct:s.stock_pct,   color:'#f59e0b'},
  ];

  document.getElementById('completionTableBody').innerHTML = rows.map(r => {
    const pending = r.total - r.done;
    return '<tr class="hover:bg-slate-800/30">'
      +'<td class="py-2.5 pr-4 font-medium" style="color:'+r.color+'">'+r.label+'</td>'
      +'<td class="py-2.5 px-3 text-right text-slate-300">'+r.total.toLocaleString()+'</td>'
      +'<td class="py-2.5 px-3 text-right text-slate-200 font-semibold">'+r.done.toLocaleString()+'</td>'
      +'<td class="py-2.5 px-3 text-right '+(pending>0?'text-red-400 font-semibold':'text-emerald-400')+'">'+pending+'</td>'
      +'<td class="py-2.5 px-3 text-right font-bold '+pctColor(r.pct)+'">'+r.pct+'%</td>'
      +'<td class="py-2.5 px-3">'+bar(r.pct, r.color)+'</td>'
      +'</tr>';
  }).join('');
}

// ── Init ──────────────────────────────────────────────────────
function renderAll(){
  renderKPI(); renderBacklog(); renderChart1(); renderChart2(); renderChart3();
  renderChart5a(); renderChart5b(); renderChart6(); renderSummaryTable(); renderHeatmap();
  buildRTPeriodOptions(); renderChart7();
  buildCSPeriodOptions(); renderCompletion();
}
initFilters();
renderAll();
</script>
</body>
</html>"""

HTML = HTML.replace('###DATA###', DATA_JS).replace('###NAMES###', NAMES_JS)

with open('dashboard2.html', 'w', encoding='utf-8') as f:
    f.write(HTML)
print('dashboard2.html built OK, size:', len(HTML)//1024, 'KB')
