
import React, {useEffect, useMemo, useState, useRef} from "react";
import {createRoot} from "react-dom/client";
import {createClient} from "@supabase/supabase-js";

const cfg = window.CREWPAD_CONFIG;
const supabase = createClient(cfg.SUPABASE_URL, cfg.SUPABASE_PUBLISHABLE_KEY);
const TENANT_CODE = cfg.DEFAULT_TENANT_CODE || "PREMAS";

const I = ({children}) => <span style={{display:"inline-flex",width:18,justifyContent:"center",fontWeight:900}}>{children}</span>;
const cn=(...x)=>x.filter(Boolean).join(" ");
const today=()=>new Date().toISOString().slice(0,10);
const fmt=(d)=> d ? new Intl.DateTimeFormat("en-IN",{dateStyle:"medium",timeStyle:"short",timeZone:"Asia/Kolkata"}).format(new Date(d)) : "—";
const fmtDateTime=(d)=>d?new Intl.DateTimeFormat("en-IN",{dateStyle:"medium",timeStyle:"short",timeZone:"Asia/Kolkata"}).format(new Date(d)):"—";
const timeOnly=(d)=> d ? new Intl.DateTimeFormat("en-IN",{hour:"2-digit",minute:"2-digit",timeZone:"Asia/Kolkata"}).format(new Date(d)) : "—";

function minutesSince(d){
  if(!d)return 0;
  return Math.max(0, Math.floor((Date.now() - new Date(d).getTime()) / 60000));
}
function runTime(d){
  const mins = minutesSince(d);
  const h = Math.floor(mins/60);
  const m = mins % 60;
  if(h <= 0) return `${m}m`;
  if(h < 24) return `${h}h ${m}m`;
  const days = Math.floor(h/24);
  const rh = h % 24;
  return `${days}d ${rh}h`;
}
function hhmmFromMinutes(mins){
  const n = Number(mins||0);
  const h = Math.floor(n/60);
  const m = n%60;
  return `${h}:${String(m).padStart(2,"0")}`;
}
function minutesToRunTime(mins){
  const n = Math.max(0, Number(mins||0));
  const h = Math.floor(n/60);
  const m = n%60;
  if(h <= 0) return `${m}m`;
  if(h < 24) return `${h}h ${m}m`;
  const d = Math.floor(h/24);
  return `${d}d ${h%24}h`;
}
const init=(n)=>String(n||"?").split(/\s+/).filter(Boolean).slice(0,2).map(x=>x[0]).join("").toUpperCase()||"?";
const loginId=(username)=>{const c=String(username||"").trim().toLowerCase().replace(/\s+/g,"");return c.includes("@")?c:`${c}@${TENANT_CODE.toLowerCase()}.local`};

const ACTIVE_EMPLOYEE_STATUSES = new Set(["active","working","working / active","probation","confirmed","on_notice","on notice"]);
const EXITED_EMPLOYEE_STATUSES = new Set(["exited","exit","inactive","separated","terminated","resigned","left","not_active","not active"]);
function normStatus(s){return String(s||"").trim().toLowerCase().replace(/[_-]+/g," ");}
function isActiveEmployeeStatus(s){
  const v=normStatus(s||"active");
  if(!v) return true;
  if(EXITED_EMPLOYEE_STATUSES.has(v)) return false;
  if(v.includes("exit") || v.includes("separat") || v.includes("terminat") || v.includes("resign")) return false;
  return ACTIVE_EMPLOYEE_STATUSES.has(v) || ["active","working"].some(x=>v.includes(x));
}
function safeDateObj(v){
  if(!v) return null;
  if(v instanceof Date && !isNaN(v.getTime())) return v;
  if(typeof v === "number" && Number.isFinite(v)){
    // Excel serial fallback for any grid/view still receiving raw serial numbers.
    try{
      if(window.XLSX?.SSF?.parse_date_code){
        const x = XLSX.SSF.parse_date_code(v);
        if(x?.y && x?.m && x?.d) return new Date(Number(x.y), Number(x.m)-1, Number(x.d));
      }
    }catch(e){}
  }
  const s=String(v).trim();
  if(!s || s==='—' || s==='-' || ['null','na','n/a'].includes(s.toLowerCase())) return null;

  // ISO / backend date: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD
  let m=s.match(/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})(?:\s.*)?$/);
  if(m) return new Date(Number(m[1]),Number(m[2])-1,Number(m[3]));

  // Indian HR upload/date display: DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY
  m=s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{4})(?:\s.*)?$/);
  if(m) return new Date(Number(m[3]),Number(m[2])-1,Number(m[1]));

  // Indian short year: DD-MM-YY / DD/MM/YY. 70-99 => 19xx, otherwise 20xx.
  m=s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{2})(?:\s.*)?$/);
  if(m){ const yy=Number(m[3]); const yyyy=yy>=70?1900+yy:2000+yy; return new Date(yyyy,Number(m[2])-1,Number(m[1])); }

  // Text dates: 05 Dec 2025 / Dec 05 2025
  const d=new Date(s);
  return isNaN(d.getTime()) ? null : d;
}
function isoDateLocal(d){
  if(!d || isNaN(d.getTime())) return "";
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
}
function nextAnnualDate(src, fromDate=new Date()){
  const d=safeDateObj(src); if(!d)return "";
  let n=new Date(fromDate.getFullYear(), d.getMonth(), d.getDate());
  const today0=new Date(fromDate.getFullYear(),fromDate.getMonth(),fromDate.getDate());
  if(n<today0) n=new Date(fromDate.getFullYear()+1, d.getMonth(), d.getDate());
  return isoDateLocal(n);
}
function daysBetweenIso(a,b){
  const da=safeDateObj(a), db=safeDateObj(b); if(!da||!db)return null;
  return Math.round((new Date(db.getFullYear(),db.getMonth(),db.getDate())-new Date(da.getFullYear(),da.getMonth(),da.getDate()))/86400000);
}

function ageOnDate(dob,eventIso){
  const b=safeDateObj(dob), e=safeDateObj(eventIso); if(!b||!e)return '';
  let age=e.getFullYear()-b.getFullYear();
  const before=(e.getMonth()<b.getMonth()) || (e.getMonth()===b.getMonth() && e.getDate()<b.getDate());
  if(before) age--;
  return age>=0 ? age : '';
}
function yearsOnDate(start,eventIso){
  const s=safeDateObj(start), e=safeDateObj(eventIso); if(!s||!e)return '';
  let y=e.getFullYear()-s.getFullYear();
  const before=(e.getMonth()<s.getMonth()) || (e.getMonth()===s.getMonth() && e.getDate()<s.getDate());
  if(before) y--;
  return y>=0 ? y : '';
}
function monthNameFromIso(iso){const d=safeDateObj(iso); return d?d.toLocaleDateString('en-IN',{month:'short',year:'numeric'}):'No date'}
function eventTypeNice(t){
  const map={birthday:'Birthday',work_anniversary:'Work Anniversary',confirmation_due:'Confirmation Due',exit_anniversary:'Exit Anniversary'};
  return map[t]||humanLabel(t||'event');
}
function eventUrgency(e,todayIso){
  const d=daysBetweenIso(todayIso,e.event_date);
  if(d==null)return 'info';
  if(d<0)return 'past'; if(d===0)return 'today'; if(d<=7)return 'this week'; if(d<=30)return 'this month'; return 'upcoming';
}
function isOnRollEmployee(e){
  const type=String(e.employee_type_code||e.employee_type||e.employee_type_name||'').toLowerCase();
  return !type || type.includes('onroll') || type.includes('on roll') || type.includes('employee') || type==='regular';
}
function firstPresent(row, keys){
  for(const k of keys){
    if(row && row[k]!==undefined && row[k]!==null && String(row[k]).trim()!=="") return row[k];
  }
  return "";
}
function employeeDob(row){
  return firstPresent(row,["date_of_birth","dob","birth_date","date_birth","employee_dob","personal_date_of_birth","birthdate","date_of_birth_text","dob_text"]);
}
function employeeDoj(row){
  return firstPresent(row,["date_of_joining","doj","joining_date","date_joined","employment_start_date","start_date","date_of_joining_text","doj_text"]);
}
function employeeDept(row){
  return firstPresent(row,["department_code","department_name","department","function","sub_department_code","sub_department_name","team_cell_code"]) || "Unmapped";
}
function employeeConfirmStatus(row){
  return String(firstPresent(row,["confirmation_status","employee_confirmation_status","confirmation_bucket","tracker_bucket"])||"").toLowerCase();
}

function valueForSort(row,key){
  const v=row?.[key]; if(key.includes("date")||key.includes("due")) return safeDateObj(v)?.getTime()||0;
  return String(v??"").toLowerCase();
}
function cmpSort(a,b,key,dir="asc"){
  const av=valueForSort(a,key), bv=valueForSort(b,key);
  const c=av>bv?1:av<bv?-1:0;
  return dir==="desc"?-c:c;
}


function useLoad(fn,deps=[]){
  const [data,setData]=useState(null),[loading,setLoading]=useState(true),[error,setError]=useState("");
  async function load(){setLoading(true);setError("");try{const r=await fn();setData(r);}catch(e){console.error(e);setError(e.message||String(e));}finally{setLoading(false);}}
  useEffect(()=>{load()},deps);
  return {data,loading,error,reload:load};
}
async function countTable(table){const {count,error}=await supabase.from(table).select("*",{count:"exact",head:true});return error?"—":count}

async function getOrgMaps(){
  const [deps,sites,desigs] = await Promise.all([
    supabase.from("departments").select("id,department_code,department_name"),
    supabase.from("sites").select("id,site_code,site_name"),
    supabase.from("designations").select("id,designation_name")
  ]);
  return {
    departments:Object.fromEntries((deps.data||[]).map(x=>[x.id,x])),
    sites:Object.fromEntries((sites.data||[]).map(x=>[x.id,x])),
    designations:Object.fromEntries((desigs.data||[]).map(x=>[x.id,x]))
  };
}
function withOrg(row,maps){
  const d = maps.departments[row.department_id] || {};
  const s = maps.sites[row.site_id] || {};
  const g = maps.designations[row.designation_id] || {};
  return {
    ...row,
    department_name:d.department_name || row.department_name || row.department || row.dept_name || row.department_text || "",
    department_code:d.department_code || row.department_code || "",
    site_name:s.site_name || row.site_name || row.site || row.work_location || row.location || "",
    site_code:s.site_code || row.site_code || "",
    designation_name:g.designation_name || row.designation_name || row.designation || ""
  };
}


const FIELD_LABELS = {
  employee_code:"Employee Code", full_name:"Full Name", status:"Employee Status", date_of_joining:"Date of Joining", last_working_day:"Last Working Day",
  site_code:"Site", division_code:"Reporting Line / Directorate", department_code:"Function / Department", sub_department_code:"Sub-Department / Workstream", team_cell_code:"Team / Cell", org_chart_node_code:"Org Chart Node",
  designation_name:"Designation", designation_track:"Designation Track", band:"Band", grade:"Grade", role_fitment_code:"Role / Grade Fitment", education_match:"Education Match", employee_type_code:"Employee Type",
  reporting_manager_code:"Reporting Manager", functional_head_code:"Functional Head", hr_partner_code:"HR Partner",
  official_email:"Official Email", personal_email:"Personal Email", personal_mobile:"Mobile", photo_path:"Photo",
  org_unit_code:"Org Unit Code", org_unit_name:"Org Unit Name", org_unit_type:"Org Unit Type", parent_org_unit_code:"Parent Org Unit", mapped_department_code:"Mapped Department",
  matrix_code:"Matrix Code", track:"Designation Track", business_designation:"Business Designation", science_designation:"Science Designation", engineering_designation:"Engineering Designation",
  education_match:"Education Match", indicative_lower_years:"Experience From", indicative_upper_years:"Experience To",
  reason_label:"Reason", reason_type:"Reason Type", setting_key:"Setting Key", setting_description:"Description", preference_key:"Preference", preference_value:"Value",
  is_active:"Active", is_enabled:"Enabled", display_order:"Display Order"
};
const FIELD_HELP = {
  employee_code:"Unique internal employee identifier. Used for import, attendance, reporting, and updates.",
  status:"Working / Active means employed and usable in attendance. Paused temporarily disables access. Exited means separated.",
  date_of_joining:"Use YYYY-MM-DD in uploads. In the UI, select using the date picker.",
  site_code:"Default/home site. Required for guard movement and inter-site transfer logic.",
  division_code:"Start from the reporting line or senior org node. Example: Director Operations, Quality, Finance, HR, IT.",
  department_code:"Next child under the selected reporting line. It may be a function or department depending on the org builder.",
  sub_department_code:"Next child under the selected function/department, such as R&D → Cell Line Development.",
  team_cell_code:"Final working team/cell where applicable, such as Mammalian Cell Culture or Yeast.",
  org_chart_node_code:"Final org structure node used for org chart placement.",
  designation_track:"Career ladder: business enablement, science-led, engineering, technician, or lab support.",
  band:"Broad leadership/career band.",
  grade:"Grade within the selected band. Used for role governance and fitment checks.",
  designation_name:"Designation title mapped to grade/track where possible.",
  role_fitment_code:"Select a valid matrix row. This auto-fills Track, Band, Grade, Designation and Education Match.",
  education_match:"Education or qualification match from the selected role-fitment matrix row.",
  employee_type_code:"Workforce category such as on-roll, trainee, contractor, vendor, consultant.",
  reporting_manager_code:"Search/select the manager. Stores employee code internally.",
  functional_head_code:"Functional reporting head. Stores employee code internally.",
  hr_partner_code:"HR partner mapped to the employee. Stores employee code internally.",
  org_unit_type:"Company, division, department, sub-department, team/cell or function.",
  parent_org_unit_code:"Parent node in the org chart. Example: R&D parent can be Director Operations.",
  mapped_department_code:"Optional link from org structure node to department master.",
  photo_path:"Upload photo through file picker. HR should not type file paths."
};
function humanLabel(key){ return FIELD_LABELS[key] || String(key||"").replace(/_/g," ").replace(/\b\w/g,m=>m.toUpperCase()); }
function Hint({field}){ const text=FIELD_HELP[field]; return text?<span className="hint" title={text}>?</span>:null; }
function FieldLabel({field,children,required=false}){ return <label className="label pretty-label">{children||humanLabel(field)} {required&&<b>*</b>} <Hint field={field}/></label>; }
function normalizeOptionLabel(x){ if(!x)return ""; return x.label||x.name||x.org_unit_name||x.department_name||x.site_name||x.designation_name||x.full_name||x.employee_type_name||x.reason_label||x.setting_key||x.preference_key||x.code||""; }
function normalizeOptionCode(x){ if(!x)return ""; return x.code||x.org_unit_code||x.department_code||x.site_code||x.designation_name||x.employee_type_code||x.employee_code||x.grade||x.band||x.track||x.reason_type||x.id||""; }
function SmartSelect({label,field,value,onChange,options=[],placeholder="Search / select...",disabled=false,required=false}){
  const [q,setQ]=useState("");
  const [open,setOpen]=useState(false);
  const boxRef=useRef(null);
  const myId=useMemo(()=>`${field}-${Math.random().toString(36).slice(2)}`,[field]);
  useEffect(()=>{const h=(e)=>{if(e.detail!==myId)setOpen(false)};window.addEventListener("smart-select-open",h);return()=>window.removeEventListener("smart-select-open",h)},[myId]);
  const selected=options.find(o=>String(normalizeOptionCode(o))===String(value))||null;
  const display=selected?`${normalizeOptionLabel(selected)}${normalizeOptionCode(selected)&&normalizeOptionCode(selected)!==normalizeOptionLabel(selected)?` (${normalizeOptionCode(selected)})`:""}`:(value||"");
  const term=(q||"").toLowerCase();
  const list=options.filter(o=>`${normalizeOptionLabel(o)} ${normalizeOptionCode(o)} ${o.parent_org_unit_code||""} ${o.org_unit_type||""}`.toLowerCase().includes(term));
  function closeSoon(){setTimeout(()=>setOpen(false),120)}
  return <div className="smart-field" ref={boxRef} onBlur={closeSoon}>
    <FieldLabel field={field} required={required}>{label}</FieldLabel>
    <div className={cn("smart-select hybrid",disabled&&"disabled")}>
      <input className="smart-input" disabled={disabled} value={open?q:display} placeholder={placeholder}
        onFocus={()=>{if(!disabled){window.dispatchEvent(new CustomEvent("smart-select-open",{detail:myId}));setOpen(true);setQ("")}}}
        onChange={e=>{setQ(e.target.value);setOpen(true)}}/>
      <button type="button" className="smart-arrow" tabIndex={-1} onMouseDown={(e)=>{e.preventDefault(); if(!disabled){window.dispatchEvent(new CustomEvent("smart-select-open",{detail:myId}));setOpen(!open);setQ("")}}}>⌄</button>
      {value&&<button type="button" className="smart-clear" title="Clear" tabIndex={-1} onMouseDown={(e)=>{e.preventDefault();e.stopPropagation();onChange("");setQ("");setOpen(false);}}>×</button>}
    </div>
    {open&&!disabled&&<div className="smart-menu">
      <div className="smart-menu-head">{list.length} option{list.length===1?"":"s"} {q?`matching “${q}”`:"available"} · type to narrow</div>
      {list.map((o,i)=><button type="button" className="smart-option" key={i} onMouseDown={(e)=>{e.preventDefault();onChange(normalizeOptionCode(o));setOpen(false);setQ("");}}>
        <b>{normalizeOptionLabel(o)}</b>
        <span>{o.pathLabel || normalizeOptionCode(o)}{o.org_unit_type?` · ${typeBadgeLabel(o.org_unit_type)}`:""}{o.parent_org_unit_code?` · under ${o.parent_org_unit_code}`:""}</span>
      </button>)}
      {!list.length&&<div className="smart-empty">No matching option. Add it under Setup Data first.</div>}
    </div>}
  </div>
}
function TextInput({field,value,onChange,type="text",required=false,placeholder=""}){ return <div><FieldLabel field={field} required={required}/><input className="input" type={type} value={value||""} placeholder={placeholder} onChange={e=>onChange(e.target.value)}/></div> }
function moduleStatusClass(v){return v?"green":"amber";}
function attentionBadge(level){
  if(level==="critical") return "critical";
  if(level==="attention") return "amber";
  return "green";
}
function statePretty(s){
  return ({OUTSIDE:"Outside",INSIDE:"Inside",ON_BREAK:"On Break",IN_TRANSIT:"In Transit",ON_OFFICIAL_DUTY:"Official Duty Out",ACCESS_BLOCKED:"Access Blocked"}[s]||s||"—");
}
function minutesLabel(m){ if(m==null)return "—"; const h=Math.floor(Number(m)/60), mm=Number(m)%60; return h?`${h}h ${mm}m`:`${mm}m`; }


function orgPathLabel(node, all){
  if(!node)return "";
  const byCode = Object.fromEntries((all||[]).map(o=>[o.org_unit_code,o]));
  const parts=[]; let cur=node; let safety=0;
  while(cur && safety++<10){
    parts.unshift(cur.org_unit_name||cur.org_unit_code);
    cur=cur.parent_org_unit_code?byCode[cur.parent_org_unit_code]:null;
  }
  return parts.join(" → ");
}
function typeBadgeLabel(t){return String(t||"").replace(/_/g," ").replace(/\b\w/g,m=>m.toUpperCase())}

async function loadSetupMasters(){
  const [sites, orgs, depts, types, desigs, grades, roleFits, emps, exits, alerts, prefs]=await Promise.all([
    supabase.from("sites").select("*").order("site_code"),
    supabase.from("v_org_units_admin").select("*").eq("is_active",true).order("display_order"),
    supabase.from("departments").select("*").order("department_name"),
    supabase.from("employee_types").select("*").order("employee_type_code"),
    supabase.from("designations").select("*").order("designation_name"),
    supabase.from("grade_designation_matrix").select("*").order("track"),
    supabase.from("v_role_fitment_options").select("*").order("track").order("band").order("grade"),
    supabase.from("employees").select("id,employee_code,full_name,department_id,site_id,status").order("employee_code"),
    supabase.from("exit_reasons").select("*").order("reason_label"),
    supabase.from("alert_settings").select("*").order("setting_key"),
    supabase.from("app_preferences").select("*").order("preference_key")
  ]);
  if(orgs.error && /v_org_units_admin/i.test(orgs.error.message||"")){
    const fallback=await supabase.from("org_units").select("*").eq("is_active",true).order("display_order");
    orgs.data=fallback.data||[]; orgs.error=null;
  }
  return {sites:sites.data||[],orgs:orgs.data||[],departments:depts.data||[],employeeTypes:types.data||[],designations:desigs.data||[],gradeMatrix:grades.data||[],roleFitments:roleFits.data||[],employees:emps.data||[],exitReasons:exits.data||[],alertSettings:alerts.data||[],preferences:prefs.data||[]};
}

function statusLabel(status){
  const s=String(status||"active");
  if(s==="active") return "Working / Active";
  if(s==="inactive") return "Paused / Inactive";
  if(s==="suspended") return "Suspended";
  if(s==="on_notice") return "On Notice";
  if(s==="exited") return "Exited";
  if(s==="draft") return "Draft / Not Joined";
  return s;
}
function employeeDataGaps(e){
  const gaps=[];
  if(!e.employee_code) gaps.push("code");
  if(!e.full_name) gaps.push("name");
  if(!e.department_name && !e.department_code && !e.department_id) gaps.push("department");
  if(!e.site_name && !e.site_code && !e.site_id) gaps.push("site");
  if(!e.designation_name && !e.designation_id) gaps.push("designation");
  if(!e.employee_type_name && !e.employee_type_code && !e.employee_type_id) gaps.push("type");
  if(!e.date_of_joining) gaps.push("DOJ");
  if(!e.official_email && !e.personal_email) gaps.push("email");
  if(!e.personal_mobile && !e.mobile) gaps.push("mobile");
  if(!e.photo_path) gaps.push("photo");
  return gaps;
}
function gapSummary(rows){
  const keys=["department","site","designation","type","DOJ","email","mobile","photo"];
  return Object.fromEntries(keys.map(k=>[k,rows.filter(e=>employeeDataGaps(e).includes(k)).length]));
}
async function uploadEmployeePhoto(file, employeeCode){
  if(!file) return "";
  const ext=(file.name.split(".").pop()||"jpg").toLowerCase();
  const safeCode=String(employeeCode||"employee").replace(/[^a-zA-Z0-9_-]/g,"_");
  const path=`employees/${safeCode}-${Date.now()}.${ext}`;
  const {error}=await supabase.storage.from("employee-photos").upload(path,file,{upsert:true});
  if(error) throw error;
  return path;
}
function boolToCsv(v){return v===true || v==="true" ? "true" : "false";}
function safeStateLabel(state){
  if(state==="INSIDE") return "Inside";
  if(state==="OUTSIDE") return "Outside";
  if(state==="ON_BREAK") return "On Break";
  if(state==="IN_TRANSIT") return "In Transit";
  return state || "Unknown";
}

function codeNum(code){
  const n = parseInt(String(code||"").replace(/\D/g,""),10);
  return Number.isFinite(n) ? n : 999999999;
}
function personMeta(e){
  const parts=[];
  if(e.department_name) parts.push(e.department_name);
  if(e.site_name || e.site_code) parts.push(e.site_name || e.site_code);
  if(e.designation_name) parts.push(e.designation_name);
  return parts.length ? parts.join(" · ") : "Master data mapping pending";
}
function empLabel(e){return e?`${e.employee_code||""}${e.employee_code?" — ":""}${e.full_name||""}`:"—"}
async function signedPhoto(path){
  if(!path)return "";
  const p=String(path||"").trim();
  if(!p)return "";
  if(/^https?:\/\//i.test(p)) return p;
  try{
    const {data,error}=await supabase.storage.from("employee-photos").createSignedUrl(p,1800);
    if(!error && data?.signedUrl) return data.signedUrl;
    const pub=supabase.storage.from("employee-photos").getPublicUrl(p);
    return pub?.data?.publicUrl || "";
  }catch(e){ return ""; }
}
function Avatar({name,path}){const [u,setU]=useState("");useEffect(()=>{signedPhoto(path).then(setU)},[path]);return u?<img className="avatar" src={u}/>:<div className="avatar">{init(name)}</div>}


function ensureGuardConsoleCss(){
  if(document.getElementById("cp-guard-console-css-v091")) return;
  const style=document.createElement("style");
  style.id="cp-guard-console-css-v091";
  style.textContent=`
  .cp-guard-console,.cp-guard-console *{box-sizing:border-box}
  .cp-guard-console{display:grid!important;gap:16px!important;width:100%!important;max-width:100%!important}
  .cp-gc-kpis{display:grid!important;grid-template-columns:repeat(6,minmax(115px,1fr))!important;gap:10px!important}
  .cp-gc-kpi{display:block!important;background:var(--card,#171a22)!important;border:1px solid var(--b0,#2a2f3a)!important;border-left:4px solid var(--gold,#c9922a)!important;border-radius:18px!important;padding:13px 14px!important;box-shadow:0 12px 30px rgba(0,0,0,.18)!important}
  .cp-gc-kpi span{display:block!important;color:var(--t2,#8f98aa)!important;font-size:10px!important;text-transform:uppercase!important;letter-spacing:.09em!important;font-weight:900!important;margin-bottom:7px!important}
  .cp-gc-kpi b{display:block!important;color:var(--t0,#f4f1e8)!important;font-family:Georgia,serif!important;font-size:32px!important;line-height:1!important}
  .cp-gc-kpi.teal{border-left-color:var(--teal,#12b8a6)!important}.cp-gc-kpi.amber,.cp-gc-kpi.danger{border-left-color:var(--amber,#f59e0b)!important}.cp-gc-kpi.danger b{color:var(--amber,#f59e0b)!important}
  .cp-gc-head{display:grid!important;grid-template-columns:1fr 350px!important;gap:18px!important;align-items:end!important;background:linear-gradient(135deg,var(--card,#171a22),rgba(201,146,42,.05))!important;border:1px solid var(--b0,#2a2f3a)!important;border-radius:24px!important;padding:18px 20px!important;box-shadow:0 12px 30px rgba(0,0,0,.18)!important}
  .cp-gc-head h2{margin:0 0 5px!important;font-family:Georgia,serif!important;font-size:34px!important;color:var(--t0,#f4f1e8)!important}.cp-gc-head p{margin:0!important;color:var(--t2,#8f98aa)!important;line-height:1.5!important}
  .cp-duty label,.cp-search-two label{display:block!important;color:var(--t2,#8f98aa)!important;font-size:10px!important;text-transform:uppercase!important;letter-spacing:.1em!important;font-weight:900!important;margin-bottom:7px!important}
  .cp-duty input,.cp-search-two input{width:100%!important;background:var(--field,#0d1017)!important;border:1px solid var(--b1,#343b49)!important;color:var(--t0,#f4f1e8)!important;border-radius:14px!important;padding:13px 14px!important;outline:none!important}
  .cp-duty input:focus,.cp-search-two input:focus{border-color:var(--gold,#c9922a)!important;box-shadow:0 0 0 3px rgba(201,146,42,.18)!important}
  .cp-gc-layout{display:grid!important;grid-template-columns:minmax(430px,1.05fr) minmax(360px,.85fr)!important;grid-template-areas:"punch visitor" "punch context"!important;gap:16px!important;align-items:start!important}
  .cp-card{display:block!important;background:var(--card,#171a22)!important;border:1px solid var(--b0,#2a2f3a)!important;border-top:3px solid var(--gold,#c9922a)!important;border-radius:22px!important;padding:18px!important;box-shadow:0 12px 30px rgba(0,0,0,.18)!important;min-width:0!important}
  .cp-punch-card{grid-area:punch!important}.cp-visitor-card{grid-area:visitor!important}.cp-context-card{grid-area:context!important}
  .cp-card-title{display:flex!important;justify-content:space-between!important;gap:12px!important;align-items:flex-start!important;margin-bottom:14px!important}
  .cp-card-title b{display:block!important;color:var(--t0,#f4f1e8)!important;font-size:18px!important;line-height:1.2!important}.cp-card-title span{display:block!important;color:var(--t2,#8f98aa)!important;font-size:12px!important;margin-top:3px!important}
  .cp-card-title em{display:inline-flex!important;align-items:center!important;justify-content:center!important;min-width:34px!important;min-height:28px!important;padding:4px 10px!important;border-radius:999px!important;background:rgba(201,146,42,.15)!important;color:var(--gold,#c9922a)!important;font-style:normal!important;font-weight:900!important;font-size:12px!important}
  .cp-search-two{display:grid!important;grid-template-columns:.7fr 1.3fr!important;gap:12px!important}
  .cp-search-meta{display:flex!important;align-items:center!important;gap:12px!important;margin:10px 0!important}.cp-search-meta button{border:1px solid var(--b0,#2a2f3a)!important;background:var(--card2,#20242e)!important;color:var(--t1,#d8d2c4)!important;border-radius:10px!important;padding:6px 11px!important;font-weight:800!important;cursor:pointer!important}.cp-search-meta span{color:var(--t2,#8f98aa)!important;font-size:12px!important}
  .cp-suggestion-header{margin:12px 0 8px!important;color:var(--t2,#8f98aa)!important;font-size:11px!important;text-transform:uppercase!important;letter-spacing:.1em!important;font-weight:900!important}.cp-suggestion-list{display:grid!important;gap:8px!important;max-height:240px!important;overflow:auto!important;padding-right:3px!important}.cp-suggestion{width:100%!important;border:1px solid var(--b0,#2a2f3a)!important;background:var(--card2,#20242e)!important;color:var(--t1,#d8d2c4)!important;border-radius:17px!important;padding:10px!important;display:grid!important;grid-template-columns:48px 1fr auto!important;gap:12px!important;align-items:center!important;text-align:left!important;cursor:pointer!important}
  .cp-suggestion:hover,.cp-suggestion.active,.cp-suggestion.selected{border-color:var(--gold,#c9922a)!important;box-shadow:0 0 0 3px rgba(201,146,42,.18)!important;background:var(--hover,#252b36)!important}.cp-suggestion .avatar{width:48px!important;height:48px!important;border-radius:14px!important}
  .cp-suggestion-main div{display:flex!important;flex-wrap:wrap!important;gap:8px!important;align-items:center!important}.cp-suggestion-main b{color:var(--t0,#f4f1e8)!important;font-size:15px!important}.cp-suggestion-main span{display:block!important;color:var(--t2,#8f98aa)!important;font-size:11px!important;margin-top:5px!important}.cp-ecode-inline{display:inline-flex!important;padding:2px 7px!important;border-radius:999px!important;background:rgba(201,146,42,.15)!important;color:var(--gold,#c9922a)!important;font-size:11px!important;font-weight:900!important}
  .cp-suggestion small,.cp-visitor-row small,.cp-mini-row small{border-radius:999px!important;padding:5px 8px!important;background:var(--chip,#252b36)!important;color:var(--t2,#8f98aa)!important;font-size:10px!important;font-weight:900!important;white-space:nowrap!important}.cp-suggestion small.inside,.cp-mini-row small{background:rgba(0,200,120,.13)!important;color:var(--teal,#12b8a6)!important}
  .cp-selected{margin-top:12px!important;margin-bottom:12px!important;border:1px solid var(--b0,#2a2f3a)!important;background:linear-gradient(135deg,var(--card2,#20242e),rgba(201,146,42,.05))!important;border-radius:19px!important;padding:14px!important;display:grid!important;grid-template-columns:70px 1fr auto!important;gap:14px!important;align-items:center!important;min-height:104px!important}.cp-selected.muted{display:block!important}.cp-selected .avatar{width:70px!important;height:70px!important;border-radius:18px!important}
  .cp-selected-main h3{margin:0 0 4px!important;color:var(--t0,#f4f1e8)!important;font-size:22px!important}.cp-selected-main p{margin:0 0 9px!important;color:var(--t2,#8f98aa)!important;font-size:13px!important}.cp-selected-main div{display:flex!important;flex-wrap:wrap!important;gap:6px!important}.cp-selected-main span{background:var(--chip,#252b36)!important;color:var(--t1,#d8d2c4)!important;border-radius:999px!important;padding:5px 9px!important;font-size:11px!important;font-weight:850!important}.cp-selected mark{background:rgba(201,146,42,.15)!important;color:var(--gold,#c9922a)!important;border-radius:999px!important;padding:7px 10px!important;font-weight:900!important}.cp-selected mark.inside{background:rgba(0,200,120,.13)!important;color:var(--teal,#12b8a6)!important}
  .cp-punch-pad{display:grid!important;grid-template-columns:repeat(4,1fr)!important;gap:10px!important;margin-top:13px!important}.cp-punch-pad button{border:1px solid var(--b0,#2a2f3a)!important;background:var(--card2,#20242e)!important;color:var(--t0,#f4f1e8)!important;border-radius:14px!important;padding:14px 10px!important;font-weight:950!important;cursor:pointer!important}.cp-punch-pad button.primary{background:linear-gradient(135deg,var(--teal,#12b8a6),#0ea5a3)!important;color:#fff!important;border-color:transparent!important}.cp-punch-pad button:disabled{opacity:.45!important;cursor:not-allowed!important}
  .cp-visitor-list,.cp-context-list{display:grid!important;gap:8px!important;max-height:355px!important;overflow:auto!important;padding-right:3px!important}.cp-visitor-row,.cp-mini-row{display:grid!important;grid-template-columns:42px 1fr auto!important;gap:10px!important;align-items:center!important;background:var(--card2,#20242e)!important;border:1px solid var(--b0,#2a2f3a)!important;border-radius:16px!important;padding:10px!important}.cp-visitor-row .avatar,.cp-mini-row .avatar{width:42px!important;height:42px!important;border-radius:13px!important}.cp-visitor-row b,.cp-mini-row b{color:var(--t0,#f4f1e8)!important;display:block!important;font-size:13px!important;line-height:1.25!important}.cp-visitor-row span,.cp-visitor-row em,.cp-mini-row span{color:var(--t2,#8f98aa)!important;display:block!important;font-size:11px!important;font-style:normal!important;margin-top:2px!important;line-height:1.35!important}.cp-visitor-row em{color:var(--gold,#c9922a)!important}.cp-visitor-row small.warn{background:rgba(245,158,11,.15)!important;color:var(--amber,#f59e0b)!important}
  .cp-context-tabs{display:grid!important;grid-template-columns:repeat(4,1fr)!important;gap:5px!important;background:var(--card2,#20242e)!important;border:1px solid var(--b0,#2a2f3a)!important;border-radius:15px!important;padding:4px!important;margin-bottom:12px!important}.cp-context-tabs button{border:0!important;background:transparent!important;color:var(--t2,#8f98aa)!important;border-radius:11px!important;padding:9px 8px!important;font-weight:900!important;cursor:pointer!important}.cp-context-tabs button.active{background:var(--card,#171a22)!important;color:var(--gold,#c9922a)!important;box-shadow:0 12px 30px rgba(0,0,0,.18)!important}
  .cp-alert-row{background:var(--card2,#20242e)!important;border:1px solid var(--b0,#2a2f3a)!important;border-radius:16px!important;padding:12px!important}.cp-alert-row small{display:inline-flex!important;border-radius:999px!important;padding:4px 8px!important;background:var(--chip,#252b36)!important;color:var(--t2,#8f98aa)!important;font-size:10px!important;font-weight:900!important}.cp-alert-row small.amber{background:rgba(245,158,11,.15)!important;color:var(--amber,#f59e0b)!important}.cp-alert-row small.goldb{background:rgba(201,146,42,.15)!important;color:var(--gold,#c9922a)!important}.cp-alert-row b{display:block!important;color:var(--t0,#f4f1e8)!important;margin:8px 0 3px!important}.cp-alert-row span{color:var(--t2,#8f98aa)!important;font-size:12px!important}
  .cp-empty{color:var(--t2,#8f98aa)!important;border:1px dashed var(--b1,#343b49)!important;border-radius:16px!important;padding:20px!important;text-align:center!important}.cp-console-error{background:rgba(239,68,68,.13)!important;color:#ff6b6b!important;border:1px solid rgba(239,68,68,.3)!important;border-radius:18px!important;padding:18px!important}
  html[data-theme="light"] .cp-card,html[data-theme="light"] .cp-gc-kpi,html[data-theme="light"] .cp-gc-head{background:#fff!important}html[data-theme="light"] .cp-gc-head{background:linear-gradient(135deg,#fff,#fffbf2)!important}html[data-theme="light"] .cp-suggestion,html[data-theme="light"] .cp-visitor-row,html[data-theme="light"] .cp-mini-row,html[data-theme="light"] .cp-alert-row{background:#fff!important}html[data-theme="light"] .cp-visitor-row em{color:#9a6700!important}html[data-theme="light"] .cp-duty input,html[data-theme="light"] .cp-search-two input{background:#fff!important;color:#101828!important}
  @media(max-width:1250px){.cp-gc-kpis{grid-template-columns:repeat(3,1fr)!important}.cp-gc-layout{grid-template-columns:1fr!important;grid-template-areas:"punch" "visitor" "context"!important}}
  @media(max-width:840px){.cp-gc-head{grid-template-columns:1fr!important}.cp-search-two{grid-template-columns:1fr!important}.cp-punch-pad{grid-template-columns:1fr 1fr!important}.cp-gc-kpis{grid-template-columns:repeat(2,1fr)!important}}
  @media(max-width:560px){.cp-gc-kpis{grid-template-columns:1fr!important}.cp-selected{grid-template-columns:56px 1fr!important}.cp-selected mark{grid-column:2!important}.cp-selected .avatar{width:56px!important;height:56px!important}.cp-visitor-row,.cp-mini-row{grid-template-columns:38px 1fr!important}.cp-visitor-row small,.cp-mini-row small{grid-column:2!important;justify-self:start!important}}

  .cp-suggestion-header{margin:14px 0 10px!important;color:var(--gold,#c9922a)!important;font-size:12px!important;text-transform:uppercase!important;letter-spacing:.12em!important;font-weight:950!important}
  .cp-suggestion-list{display:grid!important;gap:10px!important;max-height:430px!important;overflow:auto!important;padding-right:6px!important}
  .cp-suggestion{min-height:78px!important;border:1px solid rgba(201,146,42,.55)!important;background:linear-gradient(135deg,var(--card,#171a22),var(--card2,#20242e))!important;box-shadow:0 8px 22px rgba(0,0,0,.16)!important}
  .cp-suggestion .avatar{width:56px!important;height:56px!important;border-radius:16px!important}
  .cp-suggestion-main b{font-size:17px!important;color:var(--t0,#f4f1e8)!important}
  .cp-suggestion-main span{font-size:12.5px!important;color:var(--t2,#8f98aa)!important}
  .cp-ecode-inline{display:inline-flex!important;padding:3px 8px!important;border-radius:999px!important;background:rgba(201,146,42,.18)!important;color:var(--gold,#c9922a)!important;font-weight:950!important}
  .cp-selected-compact{margin-top:14px!important;margin-bottom:12px!important}
  html[data-theme="light"] .cp-suggestion{background:#fff!important;border-color:#d39b2c!important}

  `;
  document.head.appendChild(style);
}

function App(){
  const [theme,setTheme]=useState(localStorage.getItem("cp-theme")||"light");
  const [session,setSession]=useState(null),[profile,setProfile]=useState(null),[roles,setRoles]=useState([]),[tenant,setTenant]=useState(null),[loading,setLoading]=useState(true),[rolesLoaded,setRolesLoaded]=useState(false);
  const [portal,setPortal]=useState(location.hash?.replace("#/","")||"hr");

  useEffect(()=>{document.documentElement.dataset.theme=theme;localStorage.setItem("cp-theme",theme)},[theme]);
  useEffect(()=>{supabase.auth.getSession().then(({data})=>{setSession(data.session);setLoading(false)});const {data:{subscription}}=supabase.auth.onAuthStateChange((_e,s)=>setSession(s));return()=>subscription.unsubscribe()},[]);
  useEffect(()=>{if(session?.user)loadUser();else{setProfile(null);setRoles([]);setTenant(null);setRolesLoaded(false)}},[session?.user?.id]);

  function inferRolesFromEmail(email){
    const e=String(email||"").toLowerCase();
    if(e.includes("guard.")) return ["guard"];
    if(e.includes("hr.") || e.includes("admin") || e.includes("thetimelordnow")) return ["tenant_admin","hr_admin"];
    if(e.includes("manager.")) return ["employee","manager"];
    return ["employee"];
  }

  async function loadUser(){
    setRolesLoaded(false);
    const {data:p,error}=await supabase.from("profiles").select("*").eq("id",session.user.id).single();
    if(error){console.error(error);setRolesLoaded(true);return}
    const [{data:r,error:roleErr},{data:t},{data:ua}] = await Promise.all([
      supabase.from("user_roles").select("role").eq("user_id",session.user.id).eq("is_active",true),
      supabase.from("tenants").select("*").eq("id",p.tenant_id).single(),
      supabase.from("user_accounts").select("portal_access,account_status").eq("auth_user_id",session.user.id).maybeSingle()
    ]);
    if(roleErr) console.error(roleErr);
    let rr=(r||[]).map(x=>x.role).filter(Boolean);

    // Fallback only for prototype/test safety: user_roles can be blank because of partial RLS/schema state.
    if(!rr.length && ua?.portal_access?.length){
      const pa=ua.portal_access;
      if(pa.includes("hr")) rr.push("hr_team");
      if(pa.includes("guard")) rr.push("guard");
      if(pa.includes("employee")) rr.push("employee");
    }
    if(!rr.length) rr=inferRolesFromEmail(session.user.email);

    setProfile(p);
    setTenant(t);
    setRoles([...new Set(rr)]);
    setRolesLoaded(true);
  }

  if(loading)return <div className="cp-loading"><div className="cp-spinner"/><span>Loading Crewpad…</span></div>;
  if(!session)return <Login/>;
  if(!profile || !rolesLoaded)return <div className="cp-loading"><div className="cp-spinner"/><span>Loading your access profile…</span></div>;
  if(!profile)return <div className="login"><div className="brand"><div className="mark">CP</div><h1>Profile not linked</h1><p>This auth user needs a profile and role.</p></div></div>;

  const isHr = roles.some(r=>["tenant_admin","hr_admin","hr_team"].includes(r));
  const allowed = isHr ? ["hr","guard","employee"] : roles.includes("guard") ? ["guard"] : ["employee"];
  const effective = allowed.includes(portal) ? portal : allowed[0];
  if(effective!==portal)setTimeout(()=>{ setPortal(effective); location.hash=`/${effective}`; },0);

  return <Shell {...{portal:effective,setPortal,allowed,theme,setTheme,session,profile,roles,tenant}}/>;
}

function Login(){
  const [email,setEmail]=useState(""),[password,setPassword]=useState(""),[err,setErr]=useState(""),[busy,setBusy]=useState(false);
  async function submit(e){
    e.preventDefault();setErr("");setBusy(true);
    const {error}=await supabase.auth.signInWithPassword({email:email.includes("@")?email:loginId(email),password});
    setBusy(false);
    if(error)setErr(error.message);
  }
  return <div className="login">
    <div className="brand">
      <div className="brand-eyebrow"><span>Crewpad</span></div>
      <div className="mark" style={{marginBottom:0}}>CP</div>
      <h1>HR<br/>Operations</h1>
      <div className="brand-divider"/>
      <p>Attendance intelligence, gate control, leave management, visitor tracking, and emergency readiness — unified.</p>
      <div className="brand-features">
        {["Live gate punch terminal","Attendance & regularisation","Leave lifecycle management","Visitor & emergency management"].map(f=><div className="brand-feature" key={f}>{f}</div>)}
      </div>
    </div>
    <div className="loginCard">
      <h2>Welcome back</h2>
      <div className="sub">Sign in with your HR-issued username or email</div>
      <label className="label">Username / Email</label>
      <input className="input" value={email} onChange={e=>setEmail(e.target.value)} placeholder="rohan.admin or guard.p77.main" autoComplete="username"/>
      <label className="label">Password</label>
      <input className="input" type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder="••••••••" autoComplete="current-password"/>
      <button className="btn gold login-submit" onClick={submit} disabled={busy}>
        {busy?"Signing in…":"Sign in →"}
      </button>
      {err&&<p className="error" style={{marginTop:12}}>{err}</p>}
      <div className="login-footer">{TENANT_CODE} · Powered by Crewpad</div>
    </div>
  </div>
}

const navs={
  hr:[
    ["Main",[["dashboard","Dashboard","⌂"]]],
    ["People",[["employees","Employees","👥"],["access","Access & Privacy","🛡"],["orgchart","Org Studio","🧭"],["bulk","Bulk Upload","⇧"],["users","User Management","🔑"],["visitors","Frequent Visitors","🏷"]]],
    ["Lifecycle",[["lifecycle","Lifecycle Dashboard","⟳"],["confirmations","Confirmations","☑"],["events","Events Calendar","📅"],["reminders","Reminders","✉"],["manager","Manager Workbench","▣"]]],
    ["Operations",[["approvals","Approvals","✓"],["absentees","Absentee Resolution","☷"],["reports","Reports","▤"],["emergency","Emergency Roster","🚨"]]],
    ["Admin",[["setup","Setup Data","⚙"],["audit","Audit / Exports","↯"]]]
  ],
  guard:[
    ["Gate",[["punch","Punch Terminal","◎"],["inside","Who's Inside","👁"],["visitors","Visitors","🏷"],["lookup","Lookup","⌕"],["manual","Manual Entry","✎"],["emergency","Emergency","🚨"],["handover","Handover","⇄"]]]
  ],
  employee:[
    ["Self Service",[["my-dashboard","My Dashboard","⌂"],["my-access","My Access","🛡"],["my-attendance","My Attendance","◎"],["my-leaves","My Leave","◷"],["apply-leave","Apply Leave","+"],["my-requests","My Requests","✓"],["my-notifications","My Notifications","✉"],["my-profile","My Profile","👤"]]]
  ]
};

function Shell({portal,setPortal,allowed,theme,setTheme,session,profile,roles,tenant}){
  const [page,setPage]=useState(portal==="guard"?"punch":portal==="employee"?"my-dashboard":"dashboard");
  useEffect(()=>setPage(portal==="guard"?"punch":portal==="employee"?"my-dashboard":"dashboard"),[portal]);
  useEffect(()=>{location.hash=`/${portal}`},[portal]);
  const nav=navs[portal];
  const title=nav.flatMap(g=>g[1]).find(i=>i[0]===page)?.[1]||"Workspace";
  return <div className="shell">
    <aside className="side">
      <div className="sideTop">
        <div className="mark sm">CP</div>
        <div>
          <strong>{tenant?.app_display_name||tenant?.tenant_name||"Crewpad"}</strong>
          <span>{session.user.email}</span>
        </div>
      </div>
      <div className="portalTabs">
        {allowed.includes("hr")&&<button className={cn("portalTab",portal==="hr"&&"active")} onClick={()=>setPortal("hr")}>HR</button>}
        {allowed.includes("guard")&&<button className={cn("portalTab",portal==="guard"&&"active")} onClick={()=>setPortal("guard")}>Guard</button>}
        {allowed.includes("employee")&&<button className={cn("portalTab",portal==="employee"&&"active")} onClick={()=>setPortal("employee")}>Self</button>}
      </div>
      {nav.map(g=><div key={g[0]}>
        <div className="navGrp">{g[0]}</div>
        {g[1].map(([key,label,icon])=><button className={cn("nav",page===key&&"active")} key={key} onClick={()=>setPage(key)}>
          <span className="nav-icon">{icon}</span>{label}
        </button>)}
      </div>)}
      <div className="side-footer">
        v1.8.34 mobile + DOB guard + org/manager fixes · {roles.join(", ")}
      </div>
    </aside>
    <main>
      <header className="top">
        <div>
          <h1>{title}</h1>
          <p>{portal.toUpperCase()} portal · Asia/Kolkata</p>
          <PortalContextBanner portal={portal}/>
        </div>
        <div className="toolbar" style={{margin:0}}>
          <div className="theme-pill">
            <button className={theme==="light"?"active-theme":""} onClick={()=>setTheme("light")}>☀ Day</button>
            <button className={theme==="dark"?"active-theme":""} onClick={()=>setTheme("dark")}>☽ Night</button>
          </div>
          <button className="btn ghost small" onClick={()=>supabase.auth.signOut()}>⇥ Sign out</button>
        </div>
      </header>
      <div className="content"><Router portal={portal} page={page} profile={profile} roles={roles}/></div>
    </main>
  </div>
}


function PortalContextBanner({portal}){
  const {data,loading,error,reload}=useLoad(async()=>{const r=await supabase.rpc('rpc_get_current_portal_context'); if(r.error)throw r.error; return r.data||{}},[portal]);
  if(loading)return <div className="contextBanner muted">Checking access context...</div>;
  if(error)return <div className="contextBanner error">Access context unavailable: {error.message||String(error)}</div>;
  const ok=data?.ok, ctx=data?.context||{};
  if(!ok)return <div className="contextBanner critical">{data?.access_block_reason||'Employee profile not linked'} · No fallback employee context will be used.</div>;
  return <div className="contextBanner"><b>{ctx.full_name}</b> · {ctx.employee_code} · {ctx.portal_status} · {ctx.permissions?.length||0} permissions</div>;
}

function AccessGovernance({profile}){
  const [tab,setTab]=useState('provisioning');
  const [accessErr,setAccessErr]=useState('');
  const [accessModal,setAccessModal]=useState(null); // {type,emp}
  const [viewAsSuccess,setViewAsSuccess]=useState('');
  const [accessFormData,setAccessFormData]=useState({login:'',reason:'',lwd:''});
  const [q,setQ]=useState('');
  const [selected,setSelected]=useState(null);
  const [permForm,setPermForm]=useState({permission_code:'SELF_VIEW',scope_type:'self',scope_value:'',data_category_code:'BASIC_EMPLOYMENT',is_enabled:true,reason:''});
  const [viewAsReason,setViewAsReason]=useState('HR support / verification');
  const {data,loading,error,reload}=useLoad(async()=>{
    const [provision,gaps,perms,cats,privacy,acks,audit,viewas,ctx]=await Promise.all([
      supabase.from('v_employee_access_provisioning').select('*').order('employee_code').limit(5000),
      supabase.from('v_access_gap_dashboard').select('*').order('employee_code').limit(5000),
      supabase.from('portal_permission_definitions').select('*').eq('is_active',true).order('permission_group').order('permission_code'),
      supabase.from('portal_data_category_definitions').select('*').eq('is_active',true).order('sensitivity_level'),
      supabase.from('privacy_notice_versions').select('*').order('created_at',{ascending:false}).limit(20),
      supabase.from('v_privacy_acknowledgement_status').select('*').order('employee_code').limit(5000),
      supabase.from('portal_access_audit_logs').select('*').order('created_at',{ascending:false}).limit(100),
      supabase.from('hr_view_as_audit_logs').select('*').order('started_at',{ascending:false}).limit(100),
      supabase.rpc('rpc_get_current_portal_context')
    ]);
    for(const r of [provision,gaps,perms,cats,privacy,acks,audit,viewas]) if(r.error) throw r.error;
    return {provision:provision.data||[],gaps:gaps.data||[],perms:perms.data||[],cats:cats.data||[],privacy:privacy.data||[],acks:acks.data||[],audit:audit.data||[],viewas:viewas.data||[],ctx:ctx.data||{}};
  },[]);
  if(loading)return <div className="empty">Loading Access & Privacy...</div>;
  if(error)return <div className="card premium"><h3>Access & Privacy failed to load</h3><p className="error">{error.message||String(error)}</p><p className="muted">Run SQL 26 first.</p></div>;
  const rows=(data.provision||[]).filter(r=>`${r.employee_code} ${r.full_name} ${r.department_code||''} ${r.portal_status||''} ${r.login_id||''}`.toLowerCase().includes(q.toLowerCase()));
  const stats={employees:data.provision.length,active:data.provision.filter(x=>x.access_enabled).length,gaps:data.gaps.length,privacyPending:data.acks.filter(x=>x.acknowledgement_pending).length};
  async function createLogin(emp,loginId){const {error}=await supabase.rpc('rpc_create_employee_login_profile',{p_employee_id:emp.employee_id,p_login_id:loginId,p_username:loginId}); if(error)return error.message; reload(); return null;}
  async function toggleAccess(emp,enabled,reason){const {error}=await supabase.rpc('rpc_set_portal_access_enabled',{p_employee_id:emp.employee_id,p_enabled:enabled,p_reason:reason||"HR action"}); if(error)return error.message; reload(); return null;}
  async function revoke(emp,reason){const {error}=await supabase.rpc('rpc_revoke_employee_portal_access',{p_employee_id:emp.employee_id,p_reason:reason||"Access revoked by HR"}); if(error)return error.message; reload(); return null;}
  async function savePerm(){if(!selected)return; const {error}=await supabase.rpc('rpc_update_employee_access_permission',{p_employee_id:selected.employee_id,p_permission_code:permForm.permission_code,p_scope_type:permForm.scope_type,p_scope_value:permForm.scope_value||null,p_data_category_code:permForm.data_category_code||null,p_is_enabled:!!permForm.is_enabled,p_reason:permForm.reason||'HR access provisioning'}); if(error){setAccessErr(error.message);}else{setAccessErr("");reload();}}
  async function startViewAs(emp){if(!viewAsReason.trim()){setAccessErr("Reason is required.");return;} const {data:res,error}=await supabase.rpc('rpc_hr_start_view_as_employee',{p_target_employee_id:emp.employee_id,p_reason:viewAsReason,p_mode:'read_only'}); if(error){setAccessErr(error.message);}else{setAccessErr("");setViewAsSuccess(`View-As audit started. Log ID: ${res?.view_as_log_id||"—"}`);reload();}}
  const tabs=['provisioning','permissions','gaps','privacy','view-as','audit','debug'];
  return <div className="grid accessGov">
    <div className="card premium accessHero"><div><h2>Identity, Access & Privacy Governance</h2><p>Deny by default · role + scope permissions · HR view-as audited · mail dormant</p></div><div className="grid4"><Stat label="Profiles" value={stats.employees} icon="👥"/><Stat label="Access Enabled" value={stats.active} icon="✓"/><Stat label="Access Gaps" value={stats.gaps} icon="!" tone="amber"/><Stat label="Privacy Pending" value={stats.privacyPending} icon="⚑" tone="blue"/></div></div>
    {accessErr&&<div className="error" style={{padding:"8px 14px",background:"var(--red-t)",borderRadius:10,marginBottom:8}}>{accessErr} <button style={{marginLeft:8,border:0,background:"transparent",cursor:"pointer",color:"var(--red)"}} onClick={()=>setAccessErr("")}>✕</button></div>}
    {viewAsSuccess&&<div className="notice good">{viewAsSuccess} <button style={{marginLeft:8,border:0,background:"transparent",cursor:"pointer"}} onClick={()=>setViewAsSuccess("")}>✕</button></div>}
    <div className="tabs">{tabs.map(t=><button key={t} className={cn(tab===t&&'active')} onClick={()=>setTab(t)}>{humanLabel(t)}</button>)}</div>
    {tab==='provisioning'&&<div className="grid"><div className="toolbar"><input className="input" placeholder="Search employee / login / dept..." value={q} onChange={e=>setQ(e.target.value)}/><button className="btn ghost" onClick={reload}>Refresh</button></div><div className="table emp-master-grid"><table><thead><tr><th>Employee</th><th>Org</th><th>Login</th><th>Status</th><th>Scopes</th><th>Actions</th></tr></thead><tbody>{rows.map(r=><tr key={r.employee_id} className={selected?.employee_id===r.employee_id?'selectedRow':''}><td><b>{r.employee_code}</b><br/>{r.full_name}<br/><span>{r.official_email||'No official email'}</span></td><td>{r.department_code||'—'}<br/><span>{r.site_code||'Site?'}</span><br/><span>Mgr: {r.reporting_manager_name||'—'}</span></td><td>{r.login_id||'Not generated'}<br/><span>{r.username||''}</span></td><td><StatusBadge value={r.portal_status||'not_created'}/><br/><span>{r.access_enabled?'Access enabled':'Access disabled'}</span></td><td><span className={cn('badge',r.has_self_view?'green':'muted')}>Self</span><span className={cn('badge',r.has_manager_view?'green':'muted')}>Manager</span><span className={cn('badge',r.has_reportees_view?'green':'muted')}>Reportees</span><span className={cn('badge',r.has_own_dept_view?'green':'muted')}>Dept</span><span className={cn('badge',r.has_own_site_view?'green':'muted')}>Site</span><span className={cn('badge',r.has_hr_access?'amber':'muted')}>HR</span><span className={cn('badge',r.has_guard_access?'blue':'muted')}>Guard</span></td><td><div className="rowActions"><button className="btn ghost small" onClick={()=>setSelected(r)}>Edit Access</button><button className="btn ghost small" onClick={()=>createLogin(r)}>Create Login</button><button className="btn ghost small" onClick={()=>toggleAccess(r,!r.access_enabled)}>{r.access_enabled?'Disable':'Enable'}</button><button className="btn ghost small" onClick={()=>startViewAs(r)}>View As</button><button className="btn danger small" onClick={()=>revoke(r)}>Revoke</button></div></td></tr>)}</tbody></table></div></div>}
    {tab==='permissions'&&<div className="grid2"><div className="card premium"><h3>Edit Access</h3>{!selected?<p className="muted">Select an employee from Provisioning first.</p>:<><p><b>{selected.full_name}</b> · {selected.employee_code}</p><div className="formGrid"><label>Permission<select className="input" value={permForm.permission_code} onChange={e=>setPermForm({...permForm,permission_code:e.target.value})}>{data.perms.map(p=><option key={p.permission_code} value={p.permission_code}>{p.permission_group} · {p.permission_code}</option>)}</select></label><label>Scope<select className="input" value={permForm.scope_type} onChange={e=>setPermForm({...permForm,scope_type:e.target.value})}>{['self','direct_reportees','all_reportees','own_department','specific_department','own_site','specific_site','all_company','specific_employee'].map(s=><option key={s}>{s}</option>)}</select></label><label>Scope Value<input className="input" value={permForm.scope_value} onChange={e=>setPermForm({...permForm,scope_value:e.target.value})} placeholder="optional dept/site/employee code"/></label><label>Data Category<select className="input" value={permForm.data_category_code} onChange={e=>setPermForm({...permForm,data_category_code:e.target.value})}><option value="">None</option>{data.cats.map(c=><option key={c.data_category_code} value={c.data_category_code}>{c.data_category_code} · {c.sensitivity_level}</option>)}</select></label><label>Enabled<select className="input" value={String(permForm.is_enabled)} onChange={e=>setPermForm({...permForm,is_enabled:e.target.value==='true'})}><option value="true">Enabled</option><option value="false">Disabled / Revoke</option></select></label><label>Reason<input className="input" value={permForm.reason} onChange={e=>setPermForm({...permForm,reason:e.target.value})} placeholder="reason required for governance"/></label></div><button className="btn gold" onClick={savePerm}>Save Permission</button></>}</div><div className="card premium"><h3>Permission Catalogue</h3><div className="table mini"><table><thead><tr><th>Permission</th><th>Group</th><th>Scope</th><th>Sensitive</th></tr></thead><tbody>{data.perms.map(p=><tr key={p.permission_code}><td><b>{p.permission_code}</b><br/><span>{p.description}</span></td><td>{p.permission_group}</td><td>{p.default_scope_type}</td><td>{p.is_sensitive?'Yes':'No'}</td></tr>)}</tbody></table></div></div></div>}
    {tab==='gaps'&&<div className="card premium"><h3>Access Review Gaps</h3><p className="muted">Use this to catch wrong employee context, exited users with access, broad manager/HR/guard access, and missing self-view.</p><div className="table"><table><thead><tr><th>Employee</th><th>Gaps</th></tr></thead><tbody>{data.gaps.map(g=><tr key={g.employee_id}><td><b>{g.employee_code}</b><br/>{g.full_name}</td><td>{(g.access_gaps||[]).map(x=><span className="badge amber" key={x}>{x}</span>)}</td></tr>)}</tbody></table></div></div>}
    {tab==='privacy'&&<div className="grid2"><div className="card premium"><h3>Privacy Notices</h3><div className="table mini"><table><thead><tr><th>Notice</th><th>Version</th><th>Status</th><th>Effective</th></tr></thead><tbody>{data.privacy.map(p=><tr key={p.id}><td><b>{p.notice_title}</b><br/><span>{p.notice_code}</span></td><td>{p.version_no}</td><td><StatusBadge value={p.status}/></td><td>{fmtDate(p.effective_from)}</td></tr>)}</tbody></table></div></div><div className="card premium"><h3>Acknowledgement Pending</h3><div className="table mini"><table><thead><tr><th>Employee</th><th>Notice</th><th>Status</th></tr></thead><tbody>{data.acks.filter(x=>x.acknowledgement_pending).slice(0,200).map(a=><tr key={a.employee_id+'-'+a.active_notice_id}><td>{a.employee_code}<br/>{a.full_name}</td><td>{a.notice_title} {a.version_no}</td><td><span className="badge amber">Pending</span></td></tr>)}</tbody></table></div></div></div>}
    {tab==='view-as'&&<div className="grid2"><div className="card premium"><h3>HR View-As Employee</h3><p className="muted">This is not impersonation. HR remains HR. A reason is required and each session is audited.</p><label>Reason<input className="input" value={viewAsReason} onChange={e=>setViewAsReason(e.target.value)}/></label><p className="muted">Select an employee from Provisioning and click View As.</p>{selected&&<button className="btn gold" onClick={()=>startViewAs(selected)}>Start View-As for {selected.full_name}</button>}</div><div className="card premium"><h3>Recent View-As Logs</h3><div className="table mini"><table><thead><tr><th>Target</th><th>Reason</th><th>Status</th><th>Started</th></tr></thead><tbody>{data.viewas.map(v=><tr key={v.id}><td>{v.target_employee_id}</td><td>{v.reason}</td><td><StatusBadge value={v.status}/></td><td>{fmtDateTime(v.started_at)}</td></tr>)}</tbody></table></div></div></div>}
    {tab==='audit'&&<div className="card premium"><h3>Access Audit Logs</h3><div className="table"><table><thead><tr><th>When</th><th>Action</th><th>Target</th><th>Reason</th></tr></thead><tbody>{data.audit.map(a=><tr key={a.id}><td>{fmtDateTime(a.created_at)}</td><td><b>{a.action_type}</b><br/><span>{a.action_summary}</span></td><td>{a.target_employee_id||a.target_user_id||'—'}</td><td>{a.reason||'—'}</td></tr>)}</tbody></table></div></div>}
    {tab==='debug'&&<div className="card premium"><h3>Identity Debug</h3><pre className="debugPre">{JSON.stringify(data.ctx,null,2)}</pre><div className="toolbar"><button className="btn ghost" onClick={()=>downloadCsv('access_provisioning.csv',data.provision)}>Download Provisioning CSV</button><button className="btn ghost" onClick={()=>downloadCsv('access_gaps.csv',data.gaps)}>Download Gaps CSV</button></div></div>}
  </div>
}

function MyAccessPrivacy({profile}){
  const [accessNoticeErr,setAccessNoticeErr]=useState('');
  const [ackSuccess,setAckSuccess]=useState(false);
  const {data,loading,error,reload}=useLoad(async()=>{const [ctx,ack,notices]=await Promise.all([supabase.rpc('rpc_get_current_portal_context'),supabase.from('v_privacy_acknowledgement_status').select('*').limit(5),supabase.from('privacy_notice_versions').select('*').eq('status','active').limit(5)]); if(ctx.error)throw ctx.error; if(ack.error)throw ack.error; if(notices.error)throw notices.error; return {ctx:ctx.data||{},ack:ack.data||[],notices:notices.data||[]}},[]);
  if(loading)return <div className="empty">Loading access details...</div>;
  if(error)return <div className="card premium"><p className="error">{error.message||String(error)}</p></div>;
  const ctx=data.ctx?.context||{}; const pending=(data.ack||[]).find(x=>x.acknowledgement_pending); const notice=(data.notices||[])[0];
  async function ackNotice(){if(!notice){setAccessNoticeErr('No active privacy notice');return;} const {error}=await supabase.rpc('rpc_acknowledge_privacy_notice',{p_privacy_notice_version_id:notice.id}); if(error){setAccessNoticeErr(error.message);}else{setAccessNoticeErr('');setAckSuccess(true);reload();}}
  if(!data.ctx?.ok)return <div className="card premium"><h3>Access Blocked</h3><p className="error">{data.ctx?.access_block_reason||'Employee profile not linked'}</p><p className="muted">No fallback employee context is used. Contact HR to link your employee profile.</p></div>;
  return <div className="grid"><div className="card premium"><h3>My Access</h3><p><b>{ctx.full_name}</b> · {ctx.employee_code}</p><p className="muted">Portal status: {ctx.portal_status} · Access enabled: {String(ctx.access_enabled)}</p><div className="accessChips">{(ctx.permissions||[]).map(p=><span className="badge green" key={p}>{p}</span>)}</div></div><div className="card premium"><h3>Privacy Notice</h3>{pending?<><p className="warning">Privacy acknowledgement pending.</p><button className="btn gold" onClick={ackNotice}>Acknowledge Active Privacy Notice</button></>:<p className="muted">No pending privacy acknowledgement.</p>} {notice&&<details><summary>{notice.notice_title} · {notice.version_no}</summary><p>{notice.notice_text}</p></details>}</div></div>
}

function Router({portal,page,profile,roles}){
  if(portal==="guard")return <Guard page={page} profile={profile} roles={roles}/>;
  if(portal==="employee")return <Employee page={page} profile={profile}/>;
  const map={
    dashboard:<HRDashboard/>,
    employees:<Employees/>,
    access:<AccessGovernance profile={profile}/>,
    orgchart:<OrgStudioRoute/>,
    bulk:<BulkEmployeeUpload profile={profile}/>,
    users:<UsersMgmt/>,
    visitors:<FrequentVisitors/>,
    lifecycle:<LifecycleDashboard/>,
    confirmations:<ConfirmationTracker/>,
    events:<EventsCalendar/>,
    reminders:<ReminderDashboard/>,
    manager:<ManagerWorkbench/>,
    approvals:<Approvals profile={profile}/>,
    absentees:<Absentees profile={profile}/>,
    reports:<Reports/>,
    emergency:<EmergencyRoster/>,
    setup:<SetupData/>,
    audit:<AuditExports/>
  };
  return map[page]||null;
}

function Stat({label,value,icon="↯",tone=""}){
  return <div className={cn("stat-tile",tone)}>
    {icon&&<div className="tile-icon">{icon}</div>}
    <div className="tile-value">{value}</div>
    <div className="tile-label">{label}</div>
  </div>
}
function Loading({label=""}){
  return <div className="cp-loading">
    <div className="cp-spinner"/>
    <span>{label ? `Loading ${label}…` : "Loading…"}</span>
  </div>
}

/* HR */
function HRDashboard(){
  const {data,loading,error,reload}=useLoad(async()=>{
    const [inside, visitors, approvals, abs, roster, contacts, exports, employees, users] = await Promise.all([
      supabase.from("v_people_inside_now").select("*"),
      supabase.from("v_active_visitors").select("*"),
      supabase.from("v_pending_attendance_approvals").select("*"),
      supabase.from("absentee_resolutions").select("*").eq("status","unresolved"),
      supabase.from("v_emergency_roster").select("*"),
      supabase.from("emergency_contacts").select("*").eq("is_active",true),
      supabase.from("report_export_logs").select("*").order("exported_at",{ascending:false}).limit(5),
      countTable("employees"),
      countTable("user_accounts")
    ]);
    for(const r of [inside,visitors,approvals,abs,roster,contacts,exports]) if(r.error) throw r.error;
    const maps = await getOrgMaps();
    const absEmployeeIds = [...new Set((abs.data||[]).map(x=>x.employee_id).filter(Boolean))];
    let empMap = {};
    if(absEmployeeIds.length){
      const {data:empRows,error:empErr}=await supabase.from("employees").select("id,employee_code,full_name,department_id,site_id").in("id",absEmployeeIds);
      if(empErr) throw empErr;
      empMap = Object.fromEntries((empRows||[]).map(e=>[e.id,withOrg(e,maps)]));
    }
    const absRows=(abs.data||[]).map(a=>({...a, employee:empMap[a.employee_id]}));
    const bySite={};(inside.data||[]).forEach(x=>{bySite[x.site_code||"UNKNOWN"]=(bySite[x.site_code||"UNKNOWN"]||0)+1});
    return {inside:inside.data||[],visitors:visitors.data||[],approvals:approvals.data||[],abs:absRows,roster:roster.data||[],contacts:contacts.data||[],exports:exports.data||[],employees,users,bySite};
  },[]);
  if(loading)return <Loading label="HR Control Room"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  return <div>
    <div className="ops-hero">
      <div className="ops-panel">
        <h2 className="ops-title">HR Operations Control Room</h2>
        <div className="ops-sub">Live attendance, visitor presence, approvals, absentee risk and emergency readiness from Supabase operational views.</div>
        <div className="toolbar" style={{marginTop:18}}><button className="btn gold" onClick={reload}>Refresh Dashboard</button><span className="badge green">Live data connected</span></div>
      </div>
      <div className="ops-kpis">
        <div className="ops-kpi"><div className="num">{data.inside.length}</div><div className="lbl">Inside Now</div></div>
        <div className="ops-kpi"><div className="num">{data.visitors.length}</div><div className="lbl">Visitors</div></div>
        <div className="ops-kpi"><div className="num">{data.approvals.length}</div><div className="lbl">Approvals</div></div>
        <div className="ops-kpi"><div className="num">{data.abs.length}</div><div className="lbl">Unresolved</div></div>
      </div>
    </div>

    <div className="grid cols4">
      <Stat label="Employees" value={data.employees} icon="👥" tone="goldb"/>
      <Stat label="User Accounts" value={data.users} icon="🔑"/>
      <Stat label="Emergency Roster" value={data.roster.length} icon="🚨" tone="amber"/>
      <Stat label="Emergency Contacts" value={data.contacts.length} icon="☎" tone="green"/>
    </div>

    <div className="site-board" style={{marginTop:18}}>
      {Object.entries(data.bySite).map(([code,count])=><div className="site-card" key={code}><h4>{code} Occupancy</h4><div className="stat">{count}</div><div className="muted">employees currently inside</div></div>)}
      {Object.keys(data.bySite).length===0&&<div className="site-card"><h4>No Site Data</h4><div className="stat">0</div><div className="muted">No employees inside yet</div></div>}
    </div>

    <div className="grid cols2" style={{marginTop:18}}>
      <div className="card premium"><div className="cardTitle"><h3>Pending Attendance Approvals</h3><span className="badge amber">{data.approvals.length}</span></div>
        {data.approvals.length?<div className="table"><table><thead><tr><th>Employee</th><th>Type</th><th>Date</th><th>Requested</th></tr></thead><tbody>{data.approvals.map(a=><tr key={a.id}><td>{a.employee_code} — {a.full_name}</td><td>{a.request_type}</td><td>{a.work_date}</td><td>{a.requested_in_time?timeOnly(a.requested_in_time):""} {a.requested_out_time?timeOnly(a.requested_out_time):""}</td></tr>)}</tbody></table></div>:<div className="empty">No pending approvals.</div>}
      </div>
      <div className="card premium"><div className="cardTitle"><h3>Visitor Overstay / Active Visitors</h3><span className="badge amber">{data.visitors.length}</span></div>
        {data.visitors.map(v=><div className="person-row" key={v.id}><div className="avatar">{init(v.visitor_name)}</div><div><div className="name">{v.visitor_name}</div><div className="meta">{v.organization||"—"} · {v.purpose||"—"} · Host: {v.host_employee_name||v.host_name||"—"} · Inside: {runTime(v.entry_time)}</div></div><span className={cn("badge",v.overstay_flag?"amber":"green")}>{v.overstay_flag?"Overstay":"Inside"}</span></div>)}
      </div>
    </div>

    <div className="grid cols3" style={{marginTop:18}}>
      <div className="card premium"><div className="cardTitle"><h3>Unresolved Absentees</h3><span className="badge redb">{data.abs.length}</span></div>{data.abs.map(a=><div className="person-row" key={a.id}><div className="avatar">{init(a.employee?.full_name)}</div><div><div className="name">{a.employee?.employee_code} — {a.employee?.full_name}</div><div className="meta">{a.absence_date} · {a.remarks}</div></div></div>)}</div>
      <div className="card premium"><div className="cardTitle"><h3>Emergency Roles</h3><span className="badge green">{data.roster.length}</span></div><div className="pill-row">{data.roster.slice(0,12).map((r,i)=><span className="role-chip" key={i}>{r.employee_code} · {r.emergency_role}</span>)}</div></div>
      <div className="card premium"><div className="cardTitle"><h3>Recent Exports</h3><span className="badge">{data.exports.length}</span></div>{data.exports.map(x=><div className="glass-note" key={x.id} style={{marginBottom:8}}><b>{x.report_type}</b><br/>{x.file_name} · {fmt(x.exported_at)}</div>)}</div>
    </div>
  </div>;
}

function Approvals({profile}){
  const {data,loading,error,reload}=useLoad(async()=>{const {data,error}=await supabase.from("v_pending_attendance_approvals").select("*").order("submitted_at",{ascending:false});if(error)throw error;return data||[]},[]);
  async function decide(row,status){
    const remarks="HR decision";
    const {error}=await supabase.from("attendance_regularization_requests").update({status,final_decision_at:new Date().toISOString(),final_decision_by:profile.id,hr_remarks:remarks}).eq("id",row.id);
    if(error)console.error(error.message);else reload();
  }
  if(loading)return <Loading label="Approvals"/>;
  if(error)return <div className="error">{error}</div>;
  return <div className="card premium"><div className="cardTitle"><h3>Attendance Regularization Approvals</h3><span className="badge amber">{data.length}</span></div>
    {data.length?<div className="table"><table><thead><tr><th>Employee</th><th>Request</th><th>Work Date</th><th>Requested Time</th><th>Reason</th><th>Action</th></tr></thead><tbody>{data.map(r=><tr key={r.id}><td>{r.employee_code} — {r.full_name}<div className="muted">{r.department_name||""}</div></td><td>{r.request_type}</td><td>{r.work_date}</td><td>{r.requested_in_time?`IN ${timeOnly(r.requested_in_time)}`:""} {r.requested_out_time?`OUT ${timeOnly(r.requested_out_time)}`:""}</td><td>{r.reason_text}</td><td><button className="btn teal small" onClick={()=>decide(r,"approved")}>Approve</button> <button className="btn red small" onClick={()=>decide(r,"rejected")}>Reject</button></td></tr>)}</tbody></table></div>:<div className="empty">No pending approvals.</div>}
  </div>
}

function Absentees({profile}){
  const {data,loading,reload}=useLoad(async()=>{
    const {data,error}=await supabase.from("absentee_resolutions").select("*").order("absence_date",{ascending:false});
    if(error)throw error;
    const maps=await getOrgMaps();
    const ids=[...new Set((data||[]).map(x=>x.employee_id).filter(Boolean))];
    let empMap={};
    if(ids.length){
      const {data:empRows,error:empErr}=await supabase.from("employees").select("id,employee_code,full_name,department_id,site_id").in("id",ids);
      if(empErr)throw empErr;
      empMap=Object.fromEntries((empRows||[]).map(e=>[e.id,withOrg(e,maps)]));
    }
    return (data||[]).map(a=>({...a, employee:empMap[a.employee_id]}));
  },[]);
  async function resolve(row,type,remarks){const {error}=await supabase.from("absentee_resolutions").update({resolution_type:type,status:"resolved",resolved_by:profile.id,resolved_at:new Date().toISOString(),remarks:remarks||"Resolved by HR"}).eq("id",row.id);if(error)console.error(error.message);else reload();}
  if(loading)return <Loading label="Absentee Queue"/>;
  return <div className="card premium"><div className="cardTitle"><h3>Absentee Resolution</h3><span className="badge amber">{data.filter(x=>x.status==="unresolved").length} unresolved</span></div>
    <div className="table"><table><thead><tr><th>Employee</th><th>Date</th><th>Status</th><th>Current Reason</th><th>Resolve</th></tr></thead><tbody>{data.map(r=><tr key={r.id}><td>{r.employee?.employee_code} — {r.employee?.full_name}</td><td>{r.absence_date}</td><td><span className={cn("badge",r.status==="unresolved"?"redb":"green")}>{r.status}</span></td><td>{r.remarks}</td><td>{r.status==="unresolved"?<div className="toolbar" style={{margin:0}}><button className="btn ghost small" onClick={()=>resolve(r,"official_duty")}>Official Duty</button><button className="btn ghost small" onClick={()=>resolve(r,"leave_applied")}>Leave</button><button className="btn ghost small" onClick={()=>resolve(r,"not_required")}>Not Required</button></div>:"—"}</td></tr>)}</tbody></table></div>
  </div>
}

function FrequentVisitors(){
  const {data,loading,error,reload}=useLoad(async()=>{const {data,error}=await supabase.from("frequent_visitors").select("*").order("visitor_name");if(error)throw error;return data||[]},[]);
  const [q,setQ]=useState("");
  if(loading)return <Loading label="Visitors"/>;
  if(error)return <div className="error">{error}</div>;
  const rows=data.filter(v=>`${v.visitor_name} ${v.organization||""} ${v.phone||""}`.toLowerCase().includes(q.toLowerCase()));
  return <div><div className="toolbar"><input className="input" style={{maxWidth:360}} placeholder="Search visitor..." value={q} onChange={e=>setQ(e.target.value)}/><button className="btn ghost" onClick={reload}>Refresh</button></div>
    <div className="grid cols3">{rows.map(v=><div className="card premium" key={v.id}><div className="cardTitle"><h3>{v.visitor_name}</h3><span className={cn("badge",v.status==="active"?"green":"amber")}>{v.status}</span></div><p className="muted">{v.organization||"—"} · {v.phone||"—"}</p><div className="glass-note">Default host: <b>{v.default_host_name||"—"}</b><br/>Purpose: {v.default_purpose||"—"}<br/>Visits: {v.total_visits}</div></div>)}</div>
  </div>
}

function EmergencyRoster(){
  const {data,loading,error}=useLoad(async()=>{
    const [roster,contacts,muster,inside,visitors]=await Promise.all([
      supabase.from("v_emergency_roster").select("*").order("site_code"),
      supabase.from("emergency_contacts").select("*").eq("is_active",true).order("display_order"),
      supabase.from("muster_snapshots").select("*").order("snapshot_time",{ascending:false}).limit(1),
      supabase.from("v_people_inside_now").select("*"),
      supabase.from("v_active_visitors").select("*")
    ]);
    for(const r of [roster,contacts,muster,inside,visitors]) if(r.error) throw r.error;
    return {roster:roster.data||[],contacts:contacts.data||[],muster:muster.data?.[0],inside:inside.data||[],visitors:visitors.data||[]};
  },[]);
  if(loading)return <div className="empty">Loading emergency readiness...</div>;
  if(error)return <div className="error">{error}</div>;
  return <div>
    <div className="emergency-band"><div><div className="small">Emergency Muster Readiness</div><div className="big">{data.inside.length + data.visitors.length}</div><div className="small">people currently on premises</div></div><div><button className="btn gold" onClick={()=>window.print()}>Print Muster</button></div></div>
    <div className="grid cols2">
      <div className="card premium"><div className="cardTitle"><h3>Emergency Roster</h3><span className="badge green">{data.roster.length}</span></div><div className="table"><table><thead><tr><th>Employee</th><th>Role</th><th>Site</th><th>Dept</th></tr></thead><tbody>{data.roster.map((r,i)=><tr key={i}><td>{r.employee_code} — {r.full_name}</td><td><span className="role-chip">{r.emergency_role}</span></td><td>{r.site_code}</td><td>{r.department_name}</td></tr>)}</tbody></table></div></div>
      <div className="card premium"><div className="cardTitle"><h3>Emergency Contacts</h3><span className="badge">{data.contacts.length}</span></div>{data.contacts.map(c=><div className="person-row" key={c.id}><div className="avatar">☎</div><div><div className="name">{c.contact_name}</div><div className="meta">{c.contact_role} · {c.phone} · {c.email}</div></div></div>)}</div>
    </div>
  </div>
}

function Reports(){
  const [date,setDate]=useState(today());
  const {data,loading,error,reload}=useLoad(async()=>{
    const [summary,events,exports,visitors]=await Promise.all([
      supabase.from("v_attendance_daily_movement_report").select("*").eq("work_date",date).order("employee_code"),
      supabase.from("attendance_events").select("*").gte("event_time",`${date}T00:00:00+05:30`).lte("event_time",`${date}T23:59:59+05:30`).order("event_time",{ascending:false}).limit(150),
      supabase.from("report_export_logs").select("*").order("exported_at",{ascending:false}),
      supabase.from("visitor_sessions").select("*").order("entry_time",{ascending:false}).limit(50)
    ]);

    if(summary.error) throw summary.error;
    if(events.error) throw events.error;

    const maps=await getOrgMaps();
    const empIds=[...new Set((events.data||[]).map(x=>x.employee_id).filter(Boolean))];
    let empMap={};
    if(empIds.length){
      const {data:empRows,error:empErr}=await supabase.from("employees").select("id,employee_code,full_name,site_id,department_id").in("id",empIds);
      if(empErr) throw empErr;
      empMap=Object.fromEntries((empRows||[]).map(e=>[e.id,withOrg(e,maps)]));
    }
    const enriched=(events.data||[]).map(e=>({...e, employee:empMap[e.employee_id], site:maps.sites[e.site_id]}));
    return {summary:summary.data||[],events:enriched,exports:exports.data||[],visitors:visitors.data||[]};
  },[date]);

  if(loading)return <Loading label="Reports"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p><div className="warning">If this says v_attendance_daily_movement_report is missing, run SQL 06 first.</div></div>;

  const totals=data.summary.reduce((a,r)=>({
    inside:a.inside+Number(r.total_inside_minutes||0),
    outside:a.outside+Number(r.total_outside_minutes||0),
    break:a.break+Number(r.total_break_minutes||0),
    exits:a.exits+Number(r.exit_count||0),
    missing:a.missing+(r.missing_final_out?1:0)
  }),{inside:0,outside:0,break:0,exits:0,missing:0});

  return <div className="grid">
    <div className="toolbar">
      <input className="input" style={{maxWidth:180}} type="date" value={date} onChange={e=>setDate(e.target.value)}/>
      <button className="btn ghost small" onClick={()=>setDate(new Date(Date.now()-86400000).toISOString().slice(0,10))}>Yesterday</button>
      <button className="btn ghost small" onClick={()=>setDate(new Date(Date.now()-2*86400000).toISOString().slice(0,10))}>2 Days Ago</button>
      <button className="btn ghost small" onClick={()=>setDate(today())}>Today</button>
      <button className="btn ghost" onClick={reload}>Refresh</button>
      <button className="btn ghost" onClick={()=>downloadCsv(`daily-time-${date}.csv`,data.summary)}>Export Daily Time CSV</button>
      <button className="btn ghost" onClick={()=>downloadCsv(`movement-${date}.csv`,data.events)}>Export Movement CSV</button>
    </div>

    <div className="grid cols4">
      <Stat label="Employees With Events" value={data.summary.length} icon="👥"/>
      <Stat label="Total Inside HH:MM" value={hhmmFromMinutes(totals.inside)} icon="◎" tone="green"/>
      <Stat label="Outside / Free HH:MM" value={hhmmFromMinutes(totals.outside)} icon="↔" tone="amber"/>
      <Stat label="Break HH:MM" value={hhmmFromMinutes(totals.break)} icon="☕" tone="goldb"/>
    </div>

    <div className="card premium">
      <div className="cardTitle">
        <h3>Daily Time Summary</h3>
        <span className={cn("badge",totals.missing?"amber":"green")}>{totals.missing} missing final OUT</span>
      </div>
      <div className="table"><table>
        <thead><tr><th>Employee</th><th>Dept</th><th>Site</th><th>First IN</th><th>Last OUT</th><th>Inside</th><th>Outside / Free</th><th>Break</th><th>Exits</th><th>Flags</th></tr></thead>
        <tbody>{data.summary.map(r=><tr key={`${r.employee_id}-${r.work_date}`}>
          <td>{r.employee_code} — {r.full_name}</td>
          <td>{r.department_name||"—"}</td>
          <td>{r.site_code||"—"}</td>
          <td>{timeOnly(r.first_in)}</td>
          <td>{timeOnly(r.last_out)}</td>
          <td><span className="badge green">{r.total_inside_hhmm}</span></td>
          <td><span className="badge amber">{r.total_outside_hhmm}</span></td>
          <td><span className="badge goldb">{r.total_break_hhmm}</span></td>
          <td>{r.exit_count}</td>
          <td>{r.missing_final_out?<span className="badge amber">Missing OUT</span>:r.currently_inside_as_of_last_event?<span className="badge green">Inside</span>:"—"}</td>
        </tr>)}</tbody>
      </table></div>
    </div>

    <div className="card premium">
      <div className="cardTitle"><h3>Raw Movement Log</h3><span className="badge">{data.events.length}</span></div>
      <div className="table"><table>
        <thead><tr><th>Time</th><th>Employee</th><th>Event</th><th>Reason</th><th>Source</th><th>Site</th></tr></thead>
        <tbody>{data.events.map(e=><tr key={e.id}>
          <td>{fmt(e.event_time)}</td>
          <td>{e.employee?.employee_code} — {e.employee?.full_name}</td>
          <td>{String(e.event_type)}</td>
          <td>{e.reason_code||e.reason_text||"—"}</td>
          <td>{String(e.source)}</td>
          <td>{e.site?.site_code||"—"}</td>
        </tr>)}</tbody>
      </table></div>
    </div>
  </div>
}

function AuditExports(){
  const {data,loading}=useLoad(async()=>{const {data}=await supabase.from("report_export_logs").select("*").order("exported_at",{ascending:false});return data||[]},[]);
  if(loading)return <div className="empty">Loading audit/export logs...</div>;
  return <div className="card premium"><div className="cardTitle"><h3>Report Export Logs</h3><span className="badge">{data.length}</span></div><div className="table"><table><thead><tr><th>Time</th><th>Report</th><th>Format</th><th>Rows</th><th>File</th></tr></thead><tbody>{data.map(x=><tr key={x.id}><td>{fmt(x.exported_at)}</td><td>{x.report_type}</td><td>{x.export_format}</td><td>{x.row_count}</td><td>{x.file_name}</td></tr>)}</tbody></table></div></div>
}




async function loadOrgStudioData(){
  async function safe(label,promise){
    const res=await promise;
    if(res.error){console.warn("Org Studio load warning:",label,res.error);return {data:[],error:res.error.message||String(res.error)}}
    return {data:res.data||[],error:null};
  }
  const [versions,nodes,edges,gaps,summary,lenses,orgs,sites,logs]=await Promise.all([
    safe("versions",supabase.from("org_design_versions").select("*").order("created_at",{ascending:false})),
    safe("nodes",supabase.from("v_org_studio_nodes").select("*").order("position_code")),
    safe("edges",supabase.from("v_org_studio_edges").select("*")),
    safe("gaps",supabase.from("v_org_studio_gaps").select("*").order("position_code")),
    safe("summary",supabase.from("v_org_studio_summary").select("*")),
    safe("lenses",supabase.from("org_design_lenses").select("*").eq("is_active",true).order("lens_name")),
    safe("orgs",supabase.from("v_org_units_admin").select("*").order("display_order")),
    safe("sites",supabase.from("sites").select("*").order("site_code")),
    safe("logs",supabase.from("org_design_change_sets").select("*").order("created_at",{ascending:false}).limit(50))
  ]);
  return {
    versions:versions.data, nodes:nodes.data, edges:edges.data, gaps:gaps.data, summary:summary.data,
    lenses:lenses.data, orgs:orgs.data, sites:sites.data, logs:logs.data,
    loadWarnings:[versions.error,nodes.error,edges.error,gaps.error,summary.error,lenses.error,orgs.error,sites.error,logs.error].filter(Boolean)
  };
}

async function loadOrgChartData(){
  async function safe(label, promise){
    const res = await promise;
    if(res.error){
      console.warn("Org Chart load warning:", label, res.error);
      return {data:[], error:res.error.message || String(res.error)};
    }
    return {data:res.data||[], error:null};
  }
  const [nodes,edges,gaps,orgs,logs]=await Promise.all([
    safe("nodes", supabase.from("v_org_chart_nodes").select("*").order("employee_code")),
    safe("edges", supabase.from("v_org_chart_edges").select("*")),
    safe("gaps", supabase.from("v_org_chart_gaps").select("*").order("employee_code")),
    safe("orgs", supabase.from("v_org_units_admin").select("*").order("display_order")),
    safe("logs", supabase.from("org_chart_change_logs").select("*").order("created_at",{ascending:false}).limit(100))
  ]);
  return {
    nodes:nodes.data||[],
    edges:edges.data||[],
    gaps:gaps.data||[],
    orgs:orgs.data||[],
    logs:logs.data||[],
    loadWarnings:[nodes.error,edges.error,gaps.error,orgs.error,logs.error].filter(Boolean)
  };
}

function SetupData(){
  const {data,loading,error,reload}=useLoad(loadSetupMasters,[]);
  const [active,setActive]=useState("orgs");
  const [q,setQ]=useState("");
  const [form,setForm]=useState({});
  const [editing,setEditing]=useState(null);
  const [setupErr,setSetupErr]=useState("");
  if(loading)return <Loading label="Setup Data"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p></div>;

  const orgRows=(data.orgs||[]).map(o=>({...o,pathLabel:orgPathLabel(o,data.orgs)}));
  const sections=[
    {id:"orgs",title:"Org Builder",table:"org_units",rows:orgRows,desc:"Build the reporting structure your way. Any node can be a function, department, sub-department, team, cell or other unit.",fields:["org_unit_code","org_unit_name","org_unit_type","parent_org_unit_code","display_order","child_count","is_active","notes"]},
    {id:"grades",title:"Grade / Designation Matrix",table:"grade_designation_matrix",rows:data.gradeMatrix,desc:"Bands, grades, designation tracks, education match and experience range.",fields:["matrix_code","track","band","grade","business_designation","science_designation","engineering_designation","education_match","indicative_lower_years","indicative_upper_years","is_active"]},
    {id:"sites",title:"Sites",table:"sites",rows:data.sites,desc:"Physical/operational locations used for attendance and gate movement.",fields:["site_code","site_name","is_active"]},
    {id:"departments",title:"Legacy Department Master",table:"departments",rows:data.departments,desc:"Kept for compatibility. For org mapping, prefer Org Builder.",fields:["department_code","department_name","is_active"]},
    {id:"types",title:"Employee Types",table:"employee_types",rows:data.employeeTypes,desc:"On-roll, trainee, contractor/vendor and other workforce categories.",fields:["employee_type_code","employee_type_name","is_active"]},
    {id:"designations",title:"Designations",table:"designations",rows:data.designations,desc:"Designation titles. Matrix controls fitment by band/grade/track.",fields:["designation_name","is_active"]},
    {id:"exits",title:"Exit / Punch Reasons",table:"exit_reasons",rows:data.exitReasons,desc:"Reasons used for exit, punch, correction and movement logs.",fields:["reason_label","reason_type","is_active"]},
    {id:"alerts",title:"Alert Settings",table:"alert_settings",rows:data.alertSettings,desc:"Configurable alerts such as missing punch, overstay and emergency contact panels.",fields:["setting_key","setting_description","is_enabled"]},
    {id:"prefs",title:"App Preferences",table:"app_preferences",rows:data.preferences,desc:"Tenant-level preferences such as date/time format, dashboard defaults and tolerances.",fields:["preference_key","preference_value"]}
  ];
  const sec=sections.find(s=>s.id===active)||sections[0];
  const filtered=(sec.rows||[]).filter(r=>JSON.stringify(r).toLowerCase().includes(q.toLowerCase()));
  const orgTypeCounts=(data.orgs||[]).reduce((a,o)=>{a[o.org_unit_type]=(a[o.org_unit_type]||0)+1;return a},{});
  function valueFor(row,field){
    if(field==="parent_org_unit_code")return row.parent_org_unit_code||"";
    if(field==="mapped_department_code")return row.mapped_department_code||"";
    return row[field] ?? "";
  }
  function startAdd(parentCode=""){
    setEditing({new:true});
    setForm(active==="orgs"?{is_active:true,parent_org_unit_code:parentCode,org_unit_type:"department"}:{is_active:true,is_enabled:true});
  }
  function startEdit(row){setEditing(row); const f={}; sec.fields.forEach(k=>f[k]=valueFor(row,k)); setForm(f);}
  async function saveSetup(){
    try{
      const payload={...form}; delete payload.child_count; delete payload.pathLabel;
      if(sec.table==="org_units"){
        const parent=data.orgs.find(o=>o.org_unit_code===payload.parent_org_unit_code);
        const dept=data.departments.find(d=>d.department_code===payload.mapped_department_code);
        payload.parent_org_unit_id=parent?.id||null; payload.mapped_department_id=dept?.id||null;
        delete payload.parent_org_unit_code; delete payload.mapped_department_code;
        if(!payload.org_unit_code || !payload.org_unit_name){setSetupErr("Code and Name are required.");return;}
      }
      if(sec.table==="grade_designation_matrix" && !payload.matrix_code){
        payload.matrix_code=`${payload.track||"track"}_${payload.band||"band"}_${payload.grade||"grade"}_${payload.business_designation||payload.science_designation||payload.engineering_designation||"designation"}`.replace(/\s+/g,"_").toLowerCase();
      }
      const {error}=editing?.new?await supabase.from(sec.table).insert(payload):await supabase.from(sec.table).update(payload).eq("id",editing.id);
      if(error)throw error; setEditing(null); setForm({}); setSetupErr(""); reload();
    }catch(e){setSetupErr(e.message||String(e));}
  }
  async function toggleActive(row){
    const key=sec.table==="alert_settings"?"is_enabled":"is_active";
    if(!(key in row)){setSetupErr("This master has no active/inactive flag.");return}
    if(sec.table==="org_units" && row.child_count>0 && row[key]){ /* warn inline — proceeding */ }
    const {error}=await supabase.from(sec.table).update({[key]:!row[key]}).eq("id",row.id);
    if(error)setSetupErr(error.message);else reload();
  }
  function downloadTemplate(){const sample={}; sec.fields.forEach(f=>sample[f]=""); downloadCsv(`${sec.id}_template.csv`,[sample]);}
  function downloadCurrent(){downloadCsv(`${sec.id}_current.csv`,(sec.rows||[]).map(r=>{const out={};sec.fields.forEach(f=>out[f]=valueFor(r,f));return out;}));}
  function quickChild(row){setActive("orgs");startAdd(row.org_unit_code)}

  return <div className="setup-admin fantastic">
    <div className="setup-tabs">
      <div className="setup-intro">
        <b>Setup Control Center</b>
        <span>Configure masters without touching SQL.</span>
      </div>
      {sections.map(s=><button key={s.id} className={cn("setup-tab",active===s.id&&"active")} onClick={()=>{setActive(s.id);setEditing(null);setQ("");}}>
        <b>{s.title}</b><span>{s.rows?.length||0}</span>
      </button>)}
    </div>
    <div className="card premium setup-panel">
      <div className="cardTitle">
        <div><h3>{sec.title}</h3><p className="muted">{sec.desc}</p></div>
        <div className="toolbar"><button className="btn gold" onClick={()=>startAdd()}>Add</button><button className="btn ghost" onClick={downloadCurrent}>Download Current</button><button className="btn ghost" onClick={downloadTemplate}>Template</button><button className="btn ghost" onClick={reload}>Refresh</button></div>
      </div>
      {active==="orgs"&&<div className="org-summary">
        <div><b>{data.orgs?.length||0}</b><span>Total Nodes</span></div>
        {Object.entries(orgTypeCounts).map(([k,v])=><div key={k}><b>{v}</b><span>{typeBadgeLabel(k)}</span></div>)}
      </div>}
      <input className="input setup-search" placeholder={`Search ${sec.title.toLowerCase()}...`} value={q} onChange={e=>setQ(e.target.value)}/>
      {editing&&<div className="setup-edit-form">
        <div className="cardTitle"><h3>{editing.new?"Add":"Edit"} {sec.title}</h3><button className="btn ghost small" onClick={()=>setEditing(null)}>Close</button></div>
        {active==="orgs"&&<div className="org-builder-note">Choose a name, choose what it is, choose where it sits. This is intentionally flexible: a function can contain departments, a department can contain sub-departments, and any node can be edited later.</div>}
        <div className="formGrid setup-form-grid">
          {sec.fields.map(field=>{
            const val=form[field]??"";
            if(field==="child_count")return <div key={field}><FieldLabel field={field}/><input className="input" disabled value={val||0}/></div>;
            if(field==="org_unit_type")return <SmartSelect key={field} field={field} label="What is this node?" value={val} onChange={v=>setForm({...form,[field]:v})} options={["company","directorate","function","department","sub_department","team","cell","site_function","other"].map(x=>({code:x,label:typeBadgeLabel(x)}))}/>;
            if(field==="parent_org_unit_code")return <SmartSelect key={field} field={field} label="Parent / Reports Under" value={val} onChange={v=>setForm({...form,[field]:v})} options={orgRows.filter(o=>!editing?.id || o.id!==editing.id).map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name}))}/>;
            if(field==="mapped_department_code")return <SmartSelect key={field} field={field} label="Mapped Department" value={val} onChange={v=>setForm({...form,[field]:v})} options={data.departments.map(d=>({...d,code:d.department_code,label:d.department_name}))}/>;
            if(field==="track")return <SmartSelect key={field} field={field} label="Designation Track" value={val} onChange={v=>setForm({...form,[field]:v})} options={["business_enablement","science_led","engineering","technician","lab_support","general"].map(x=>({code:x,label:x.replace(/_/g," ")}))}/>;
            if(field==="is_active"||field==="is_enabled")return <label key={field} className="checkLine"><input type="checkbox" checked={!!val} onChange={e=>setForm({...form,[field]:e.target.checked})}/>{humanLabel(field)}</label>;
            return <TextInput key={field} field={field} value={val} onChange={v=>setForm({...form,[field]:v})}/>;
          })}
        </div>
        {setupErr&&<div className="error" style={{marginTop:8}}>{setupErr}</div>}
        <div className="toolbar"><button className="btn gold" onClick={()=>{setSetupErr("");saveSetup();}}>Save</button><button className="btn ghost" onClick={()=>{setEditing(null);setSetupErr("");}}>Cancel</button></div>
      </div>}
      <div className="table setup-table"><table>
        <thead><tr>{sec.fields.map(f=><th key={f}>{humanLabel(f)} <Hint field={f}/></th>)}{active==="orgs"&&<th>Path</th>}<th>Actions</th></tr></thead>
        <tbody>{filtered.map(row=><tr key={row.id||JSON.stringify(row)}>
          {sec.fields.map(f=><td key={f}>{f==="org_unit_type"?<span className="badge goldb">{typeBadgeLabel(valueFor(row,f))}</span>:String(valueFor(row,f)??"")}</td>)}
          {active==="orgs"&&<td className="path-cell">{row.pathLabel}</td>}
          <td><div className="toolbar" style={{margin:0}}><button className="btn ghost small" onClick={()=>startEdit(row)}>Edit</button>{active==="orgs"&&<button className="btn ghost small" onClick={()=>quickChild(row)}>Add Child</button>}<button className="btn ghost small" onClick={()=>toggleActive(row)}>Toggle</button></div></td>
        </tr>)}</tbody>
      </table></div>
    </div>
  </div>
}




function OrgAvatar({name,path,compact=false}){
  const [src,setSrc]=useState("");
  const [failed,setFailed]=useState(false);
  useEffect(()=>{let live=true; setFailed(false); setSrc(""); signedPhoto(path).then(u=>{if(live)setSrc(u||"")}).catch(()=>{if(live)setFailed(true)}); return()=>{live=false}},[path]);
  if(src && !failed) return <img className={cn("org-avatar-img",compact&&"compact")} src={src} onError={()=>setFailed(true)} loading="lazy"/>;
  return <div className={cn("org-avatar-fallback",compact&&"compact")}>{init(name)}</div>;
}
function orgStatusBadge(status){
  return status==="vacant"?"vacant":status==="planned"?"planned":status==="frozen"?"frozen":status==="inactive"?"muted":"green";
}
function lensBadgeClass(lens,node){
  if(lens==="vacancy") return orgStatusBadge(node.position_status);
  if(lens==="confirmation"){
    if(node.confirmation_status==="confirmed") return "green";
    if(["probation","unconfirmed"].includes(node.confirmation_status)) return "amber";
    if(node.probation_end_date && new Date(node.probation_end_date)<new Date()) return "critical";
    return "muted";
  }
  if(lens==="succession"){
    if(["ready_now","ready_6_12"].includes(node.succession_status)) return "green";
    if(node.succession_status==="external_hire") return "blue";
    if(node.criticality==="critical") return "critical";
    return "muted";
  }
  if(lens==="span"){
    const c=Number(node.direct_position_count||0);
    if(c>=10) return "critical";
    if(c>=8 || c<=1) return "amber";
    return "green";
  }
  if(lens==="safety"){
    if(node.is_first_aider||node.is_fire_marshal||node.is_field_marshal) return "green";
    return "muted";
  }
  if(lens==="grade_band") return node.grade?"blue":"muted";
  if(lens==="location") return node.site_code==="P77"?"blue":node.site_code==="P79"?"green":"muted";
  return "muted";
}
function lensText(lens,node){
  if(lens==="vacancy") return node.position_status || "unknown";
  if(lens==="confirmation") return node.confirmation_status || "unknown";
  if(lens==="succession") return node.succession_status || "not assessed";
  if(lens==="span") return `${node.direct_position_count||0} direct`;
  if(lens==="safety"){
    const arr=[]; if(node.is_first_aider)arr.push("First Aider"); if(node.is_fire_marshal)arr.push("Fire Marshal"); if(node.is_field_marshal)arr.push("Field Marshal");
    return arr.join(", ") || "No safety role";
  }
  if(lens==="grade_band") return `${node.band||"Band?"} / ${node.grade||"Grade?"}`;
  if(lens==="location") return node.site_code || "site missing";
  if(lens==="employee_type") return node.employee_type_code || "type missing";
  return node.position_status || "—";
}
function copyTextToClipboard(txt){
  navigator.clipboard?.writeText(txt).catch(()=>{const ta=document.createElement("textarea");ta.value=txt;document.body.appendChild(ta);ta.select();document.execCommand("copy");ta.remove();});
}

class OrgChartErrorBoundary extends React.Component{
  constructor(props){super(props);this.state={error:null}}
  static getDerivedStateFromError(error){return {error}}
  componentDidCatch(error,info){console.error("OrgChart render error",error,info)}
  render(){
    if(this.state.error){
      return <div className="card premium"><h3>Org Chart could not render</h3><p className="error">{String(this.state.error?.message||this.state.error)}</p><p className="muted">The data route is working. This is a frontend render issue. Use v1.5.2 patch and refresh.</p></div>
    }
    return this.props.children;
  }
}

function OrgStudioRoute(){return <OrgChartErrorBoundary><OrgStudioModule/></OrgChartErrorBoundary>}

function OrgStudioModule(){
  const {data,loading,error,reload}=useLoad(loadOrgStudioData,[]);
  const [tab,setTab]=useState("workspace");
  const [orgMode,setOrgMode]=useState("main");
  const [studioErr,setStudioErr]=useState("");
  const [studioModal,setStudioModal]=useState(null); // {type:"draft"|"commit"|"quickadd", anchor?, mode?}
  const [lens,setLens]=useState("grade_band");
  const [view,setView]=useState("compact");
  const [versionId,setVersionId]=useState("");
  const [q,setQ]=useState("");
  const [collapsed,setCollapsed]=useState({});
  const [selected,setSelected]=useState(null);
  const [ctx,setCtx]=useState(null);
  const [zoom,setZoom]=useState(0.72);
  const [cardSize,setCardSize]=useState("small");
  const [density,setDensity]=useState("normal");
  const [pan,setPan]=useState({x:0,y:0});
  const [dragging,setDragging]=useState(null);
  const canvasRef=useRef(null);
  const [focusId,setFocusId]=useState(null);
  const [debug,setDebug]=useState(false);
  const [maxDepth,setMaxDepth]=useState(3);
  const [editOpen,setEditOpen]=useState(false);
  const [editDraft,setEditDraft]=useState({});

  if(loading)return <div className="empty">Loading Org Studio...</div>;
  if(error)return <div className="card premium"><h3>Org Studio failed to load</h3><p className="error">{error}</p><p className="muted">Run SQL 23 first, then hard refresh.</p></div>;

  const versions=data.versions||[];
  const defaultVersion=versions.find(v=>v.status==="live")||versions[0]||null;
  const activeVersionId=versionId||defaultVersion?.id||"";
  const rawNodes=(data.nodes||[]).filter(n=>!activeVersionId || n.version_id===activeVersionId);
  const edges=(data.edges||[]).filter(e=>!activeVersionId || e.version_id===activeVersionId);
  const gaps=(data.gaps||[]).filter(g=>!activeVersionId || g.version_id===activeVersionId);
  const summary=(data.summary||[]).find(s=>s.version_id===activeVersionId)||{};
  const lenses=data.lenses||[];
  const loadWarnings=data.loadWarnings||[];

  function edgeParentId(e){return e.parent_position_id||e.parent_id||e.manager_position_id||e.source_position_id||e.source||e.from||null}
  function edgeChildId(e){return e.child_position_id||e.child_id||e.report_position_id||e.target_position_id||e.target||e.to||null}

  const parentByChild={};
  edges.forEach(ed=>{
    const p=edgeParentId(ed), c=edgeChildId(ed);
    if(p&&c&&p!==c&&!parentByChild[c]) parentByChild[c]=p;
  });

  const nodesAll=rawNodes.map(n=>{
    const derivedParent=parentByChild[n.position_id] || n.parent_position_id || null;
    return {...n,parent_position_id:derivedParent,parent_position_title:n.parent_position_title};
  });

  const byId=Object.fromEntries(nodesAll.map(n=>[n.position_id,n]));
  const childMap={};
  nodesAll.forEach(n=>{
    const parentId=n.parent_position_id;
    if(parentId && byId[parentId]){
      childMap[parentId]=childMap[parentId]||[];
      childMap[parentId].push(n);
    }
  });
  nodesAll.forEach(n=>{
    if(!n.parent_position_id || !byId[n.parent_position_id]){
      childMap.ROOT=childMap.ROOT||[];
      childMap.ROOT.push(n);
    }
  });
  Object.values(childMap).forEach(arr=>arr.sort((a,b)=>String(a.position_title||"").localeCompare(String(b.position_title||""))));
  const derivedRootCount=(childMap.ROOT||[]).length;

  function countDescendants(n,seen=new Set()){
    if(!n||seen.has(n.position_id)) return 0;
    seen.add(n.position_id);
    return (childMap[n.position_id]||[]).reduce((sum,c)=>sum+1+countDescendants(c,seen),0);
  }
  const rootsRanked=[...(childMap.ROOT||[])].map(r=>({...r,_descendantCount:countDescendants(r)})).sort((a,b)=>(b._descendantCount||0)-(a._descendantCount||0)||String(a.position_title||"").localeCompare(String(b.position_title||"")));
  const mainRoot=rootsRanked[0]||null;
  const orphanRoots=rootsRanked.filter(r=>r.position_id!==mainRoot?.position_id);
  const departmentGroups=Object.values(nodesAll.reduce((acc,n)=>{
    const key=n.org_unit_name||n.department_code||n.position_family||"Unmapped";
    acc[key]=acc[key]||{key,nodes:[]};
    acc[key].nodes.push(n);
    return acc;
  },{})).sort((a,b)=>b.nodes.length-a.nodes.length);


  const searchTerm=q.trim().toLowerCase();
  const filterNode=n=>{
    if(!searchTerm)return true;
    return [n.position_code,n.position_title,n.full_name,n.employee_code,n.org_unit_name,n.site_code,n.grade,n.band].join(" ").toLowerCase().includes(searchTerm);
  };

  function visibleRoots(){
    let roots=(childMap.ROOT&&childMap.ROOT.length)?childMap.ROOT:nodesAll.filter(n=>!n.parent_position_id || !byId[n.parent_position_id]);
    if(focusId && byId[focusId]) return [byId[focusId]];
    if(searchTerm){
      const matching=nodesAll.filter(filterNode);
      return matching.length?matching:roots;
    }
    if(orgMode==="main" && mainRoot) return [mainRoot];
    if(orgMode==="orphans") return orphanRoots.length?orphanRoots:roots;
    return roots;
  }

  function shouldRender(n){
    if(searchTerm)return filterNode(n);
    return true;
  }

  function toggleCollapse(id){setCollapsed({...collapsed,[id]:!collapsed[id]})}
  function collapseAll(){const c={};nodesAll.forEach(n=>{if((childMap[n.position_id]||[]).length)c[n.position_id]=true});setCollapsed(c)}
  function expandAll(){setCollapsed({})}
  function expandOneLevel(){
    const c={};
    const roots=visibleRoots();
    function walk(n,depth=0){
      (childMap[n.position_id]||[]).forEach(k=>{
        if(depth>=1) c[k.position_id]=true;
        walk(k,depth+1);
      });
    }
    roots.forEach(r=>walk(r,0));
    setCollapsed(c);
  }
  function collapseBelow(level=2){
    const c={};
    const roots=visibleRoots();
    function walk(n,depth=0){
      const kids=childMap[n.position_id]||[];
      if(depth>=level-1 && kids.length) c[n.position_id]=true;
      kids.forEach(k=>walk(k,depth+1));
    }
    roots.forEach(r=>walk(r,0));
    setCollapsed(c);
  }
  function focusBranch(n){setFocusId(n.position_id);setCollapsed({});setPan({x:0,y:0})}
  function resetCanvas(){setZoom(.72);setPan({x:0,y:0});setFocusId(null);setTimeout(()=>collapseBelow(2),0)}
  function fitCompact(){setCardSize("micro");setDensity("tight");setZoom(.58);setPan({x:0,y:0});setTimeout(()=>collapseBelow(2),0)}
  function handleCanvasMouseDown(e){
    if(e.button!==0)return;
    if(e.target.closest(".studio-card")||e.target.closest(".studio-canvas-tools")||e.target.closest("button"))return;
    setDragging({sx:e.clientX,sy:e.clientY,px:pan.x,py:pan.y});
  }
  function handleCanvasMouseMove(e){
    if(!dragging)return;
    setPan({x:dragging.px + e.clientX-dragging.sx, y:dragging.py + e.clientY-dragging.sy});
  }
  function handleCanvasWheel(e){
    if(e.ctrlKey || e.metaKey){
      e.preventDefault();
      const delta=e.deltaY>0?-0.06:0.06;
      setZoom(z=>Math.max(.35,Math.min(1.8,Number((z+delta).toFixed(2)))));
    }
  }

  function makeHierarchyText(mode="position"){
    const lines=[];
    function walk(n,depth=0){
      const indent="    ".repeat(depth);
      const occupant=n.full_name?` — ${n.full_name}`:n.position_status==="vacant"?" [Vacant]":n.position_status==="planned"?" [Planned]":"";
      const label=mode==="people"?(n.full_name||`${n.position_title} [${n.position_status}]`):`${n.position_title}${occupant}`;
      lines.push(indent+label);
      (childMap[n.position_id]||[]).forEach(c=>walk(c,depth+1));
    }
    visibleRoots().forEach(r=>walk(r,0));
    return lines.join("\n");
  }

  function exportHierarchy(){const txt=makeHierarchyText(view==="people"?"people":"position"); copyTextToClipboard(txt); setStudioErr("Hierarchy text copied to clipboard. Paste into PowerPoint SmartArt.");}
  function exportCsv(){
    downloadCsv("org_studio_positions.csv",nodesAll.map(n=>({
      version:n.version_name, position_code:n.position_code, position_title:n.position_title,
      parent_position_code:n.parent_position_code, occupant:n.full_name, employee_code:n.employee_code,
      status:n.position_status, band:n.band, grade:n.grade, org_unit:n.org_unit_name, site:n.site_code,
      lens:lensText(lens,n)
    })));
  }
  function exportDebug(){downloadCsv("org_studio_debug.csv",[{versions:versions.length,nodes:nodesAll.length,edges:edges.length,gaps:gaps.length,warnings:loadWarnings.join(" | ")}]);}

  function promptDraftVersion(){
    const name=window.prompt("Draft version name", `Org Draft ${new Date().toISOString().slice(0,10)}`);
    if(!name)return;
    const purpose=window.prompt("Purpose / design principle", "Org structure review");
    createDraft(name,purpose||"Org structure review");
  }
  function promptCommitVersion(){
    const note=window.prompt("Commit note / reason", "Approved org structure update");
    if(!note)return;
    commitVersion(note);
  }
  function promptQuickAdd(anchor,mode){
    const title=window.prompt(`Position title to add ${mode}`, mode==="below"?`Reportee under ${anchor.position_title}`:`Position ${mode} ${anchor.position_title}`);
    if(!title)return;
    const status=window.prompt("Status: occupied / vacant / planned / frozen", "planned") || "planned";
    quickAdd(anchor,mode,title,status);
  }

  async function createDraft(name,purpose){if(!name)return;
    const {error,data:res}=await supabase.rpc("rpc_org_studio_create_version",{p_version_name:name,p_purpose:purpose,p_design_principle:purpose,p_based_on_version_code:defaultVersion?.version_code||"LIVE"});
    if(error){setStudioErr(error.message);return}
    setStudioErr("");
    reload();
  }

  async function commitVersion(note){
    if(!activeVersionId||!note)return;
    const {error,data:res}=await supabase.rpc("rpc_org_studio_commit_version",{p_version_id:activeVersionId,p_effective_date:new Date().toISOString().slice(0,10),p_commit_note:note});
    if(error){setStudioErr(error.message);return}
    setStudioErr(`Committed. Employees updated: ${res?.employees_updated??"check logs"}`);
    reload();
  }

  async function quickAdd(anchor,mode,title,status){
    if(!title)return;
    const {error}=await supabase.rpc("rpc_org_studio_quick_add_position",{
      p_version_id:activeVersionId,
      p_anchor_position_id:anchor.position_id,
      p_add_mode:mode,
      p_position_title:title,
      p_position_status:status,
      p_band:anchor.band||null,
      p_grade:anchor.grade||null,
      p_site_id:anchor.site_id||null,
      p_org_unit_id:anchor.org_unit_id||null,
      p_reason:`Quick add ${mode} ${anchor.position_code}`
    });
    if(error){setStudioErr(error.message||String(error));return}
    reload();
  }

  function openEditPosition(n){
    setSelected(n);
    setEditDraft({
      position_title:n.position_title||"",
      position_status:n.position_status||"occupied",
      band:n.band||"",
      grade:n.grade||"",
      designation_track:n.designation_track||"",
      notes:n.notes||""
    });
    setEditOpen(true);
    setCtx(null);
  }

  async function saveEditPosition(){
    if(!selected)return;
    const payload={
      position_title:editDraft.position_title||selected.position_title,
      position_status:editDraft.position_status||selected.position_status,
      band:editDraft.band||null,
      grade:editDraft.grade||null,
      designation_track:editDraft.designation_track||null,
      notes:editDraft.notes||null,
      updated_at:new Date().toISOString()
    };
    const {error}=await supabase.from("org_positions").update(payload).eq("id",selected.position_id);
    if(error){setStudioErr(error.message);return}
    setEditOpen(false);
    reload();
  }

  async function deactivatePosition(n){
    const {error}=await supabase.from("org_positions").update({is_active:false,position_status:"inactive",updated_at:new Date().toISOString()}).eq("id",n.position_id);
    if(error){setStudioErr(error.message);return}
    setCtx(null); reload();
  }

  async function detachFromParent(n){
    const {error}=await supabase.from("org_position_relationships").delete().eq("version_id",activeVersionId).eq("child_position_id",n.position_id);
    if(error){setStudioErr(error.message);return}
    setCtx(null); reload();
  }

  function showCrudRoadmap(){ setStudioErr("CRUD: edit, deactivate, detach, focus branch, quick-add available. Next: assign employee, move with audit, vacancy approval, versioned commit."); }

  function card(n,depth=0){
    if(!shouldRender(n))return null;
    const kids=childMap[n.position_id]||[];
    const isCollapsed=collapsed[n.position_id];
    const badgeClass=lensBadgeClass(lens,n);
    return <div className="studio-node-wrap" key={n.position_id}>
      <div className={cn("studio-card",view,cardSize,density,n.position_status,badgeClass)} onContextMenu={e=>{e.preventDefault();setCtx({x:e.clientX,y:e.clientY,node:n})}} onClick={()=>setSelected(n)}>
        <div className="studio-card-top">
          {view!=="compact"&&cardSize!=="micro"&&<OrgAvatar name={n.full_name||n.position_title} path={n.photo_path} compact={view!=="photo"||cardSize==="small"}/>}
          <div className="studio-main">
            <b>{view==="people"?(n.full_name||"Vacant"):(n.position_title||"Position")}</b>
            <span>{view==="people"?n.position_title:(n.full_name||n.position_status)}</span>
            <em>{n.position_code} · {n.site_code||"Site?"}</em>
          </div>
          <button className="studio-expand" onClick={(e)=>{e.stopPropagation();toggleCollapse(n.position_id)}}>{kids.length?isCollapsed?"+":"−":"·"}</button>
        </div>
        <div className="studio-badges">
          <span className={cn("badge",badgeClass)}>{lensText(lens,n)}</span>
          {kids.length>0&&<span className="badge muted">{kids.length} below</span>}
          {n.open_comment_count>0&&<span className="badge amber">{n.open_comment_count} comment</span>}
        </div>
      </div>
      {kids.length>0&&!isCollapsed&&depth+1<maxDepth&&<div className="studio-children">{kids.map(c=>card(c,depth+1))}</div>}
      {kids.length>0&&!isCollapsed&&depth+1>=maxDepth&&<button className="studio-more" onClick={()=>{setFocusId(n.position_id);setMaxDepth(3);setPan({x:0,y:0})}}>+ {kids.length} hidden below · focus</button>}
    </div>
  }

  const tabs=["workspace","explorer","positions","vacancies","gaps","versions","exports","debug"];

  return <div className="org-studio" onClick={()=>setCtx(null)}>
    <div className="studio-hero card premium">
      <div>
        <h2>Crewpad Org Studio</h2>
        <p>Design your organization before changing your organization.</p>
        {loadWarnings.length>0&&<p className="error">Load warning: {loadWarnings[0]}</p>}
      </div>
      <div className="studio-metrics">
        <div><b>{summary.total_positions||nodesAll.length}</b><span>Positions</span></div>
        <div><b>{summary.occupied_positions||0}</b><span>Occupied</span></div>
        <div><b>{summary.vacant_positions||0}</b><span>Vacant</span></div>
        <div><b>{gaps.length}</b><span>Gaps</span></div>
      </div>
    </div>

    <div className="studio-controlbar card">
      <div><label>Version</label><select value={activeVersionId} onChange={e=>setVersionId(e.target.value)}>{versions.map(v=><option key={v.id} value={v.id}>{v.version_name} · {v.status}</option>)}</select></div>
      <div><label>Apply Lens</label><select value={lens} onChange={e=>setLens(e.target.value)}>{["vacancy","location","confirmation","succession","grade_band","span","safety","employee_type"].map(x=><option key={x} value={x}>{humanLabel(x)}</option>)}</select></div>
      <div><label>Apply View</label><select value={view} onChange={e=>setView(e.target.value)}>{["compact","photo","people","position"].map(x=><option key={x} value={x}>{humanLabel(x)}</option>)}</select></div>
      <div><label>Card Size</label><select value={cardSize} onChange={e=>setCardSize(e.target.value)}>{["micro","small","medium","large"].map(x=><option key={x} value={x}>{humanLabel(x)}</option>)}</select></div>
      <div><label>Density</label><select value={density} onChange={e=>setDensity(e.target.value)}>{["ultra","tight","normal","spacious"].map(x=><option key={x} value={x}>{humanLabel(x)}</option>)}</select></div>
      <div><label>Tree Mode</label><select value={orgMode} onChange={e=>{setOrgMode(e.target.value);setFocusId(null);setTimeout(()=>collapseBelow(2),0)}}>{[["main","Main Tree"],["all","All Roots"],["orphans","Unmapped/Other Roots"],["departments","Department Blocks"]].map(([id,label])=><option key={id} value={id}>{label}</option>)}</select></div>
      <div><label>Levels</label><select value={maxDepth} onChange={e=>setMaxDepth(Number(e.target.value))}>{[[2,"2 levels"],[3,"3 levels"],[4,"4 levels"],[99,"All"]].map(([id,label])=><option key={id} value={id}>{label}</option>)}</select></div>
      <div className="studio-search"><label>Search</label><input value={q} onChange={e=>setQ(e.target.value)} placeholder="employee, position, grade, site..."/></div>
      <button className="btn gold" onClick={promptDraftVersion}>New Draft</button>
      <button className="btn ghost" onClick={promptCommitVersion}>Commit</button>
    </div>

    <div className="studio-tabs">
      {tabs.map(t=><button key={t} className={cn(tab===t&&"active")} onClick={()=>setTab(t)}>{humanLabel(t)}</button>)}
    </div>

    {tab==="workspace"&&<div className="studio-workspace">
      <div className="studio-outline card">
        <h3>Navigator</h3>
        <button className="btn ghost small" onClick={()=>setFocusId(null)}>Show All</button>
        <p className="muted small">Roots: {derivedRootCount} · Main branch: {mainRoot?countDescendants(mainRoot)+1:0} positions</p>
        {rootsRanked.map(r=><button key={r.position_id} onClick={()=>focusBranch(r)}>{r.position_title}<span>{countDescendants(r)}</span></button>)}
      </div>
      <div className="studio-canvas card">
        {orgMode==="departments"&&<div className="department-blocks">
          {departmentGroups.map(g=><div key={g.key} className="dept-block"><b>{g.key}</b><span>{g.nodes.length} positions</span><div>{g.nodes.slice(0,12).map(n=><button key={n.position_id} onClick={()=>focusBranch(n)}>{n.full_name||n.position_title}</button>)}</div></div>)}
        </div>}
        {orgMode!=="departments"&&<div className="studio-canvas-tools">
          <button onClick={()=>setZoom(Math.max(.35,Number((zoom-.1).toFixed(2))))}>−</button><span>{Math.round(zoom*100)}%</span><button onClick={()=>setZoom(Math.min(1.8,Number((zoom+.1).toFixed(2))))}>+</button>
          <button onClick={()=>setPan(p=>({...p,x:p.x-180}))}>←</button><button onClick={()=>setPan(p=>({...p,y:p.y-140}))}>↑</button><button onClick={()=>setPan(p=>({...p,y:p.y+140}))}>↓</button><button onClick={()=>setPan(p=>({...p,x:p.x+180}))}>→</button>
          <button onClick={expandOneLevel}>One Level</button><button onClick={expandAll}>Expand</button><button onClick={collapseAll}>Collapse</button><button onClick={resetCanvas}>Fit All</button><button onClick={fitCompact}>Compact Fit</button>
          <button onClick={exportHierarchy}>Copy PPT Text</button>
          <button onClick={()=>collapseBelow(2)}>Collapse to 2 Levels</button><button onClick={()=>{setCardSize("small");setDensity("normal");setZoom(.85)}}>Readable</button>
          <span className="studio-nav-tip">Main Tree shows largest branch · use Tree Mode for all/orphans</span>
        </div>}
        {orgMode!=="departments" && (nodesAll.length===0?<div className="empty"><b>No Org Studio data.</b><br/>Run SQL 28 sync, then refresh.</div>:visibleRoots().length===0?<div className="empty"><b>Org data loaded but no root could be derived.</b><br/>Nodes: {nodesAll.length} · Edges: {edges.length}. Open Debug tab and check edge field names.</div>:<div className="studio-pan-surface" ref={canvasRef} onMouseDown={handleCanvasMouseDown} onMouseMove={handleCanvasMouseMove} onMouseUp={()=>setDragging(null)} onMouseLeave={()=>setDragging(null)} onWheel={handleCanvasWheel}><div className={cn("studio-tree",density,dragging&&"dragging","auto-tree")} style={{transform:`translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,transformOrigin:"top center"}}>{visibleRoots().map(r=>card(r))}</div></div>)}
      </div>
      <div className="studio-detail card">
        <h3>Details</h3>
        <div className="studio-mini-nav"><b>Canvas</b><span>Zoom {Math.round(zoom*100)}%</span><span>X {Math.round(pan.x)} · Y {Math.round(pan.y)}</span><button className="btn ghost small" onClick={resetCanvas}>Reset</button><button className="btn ghost small" onClick={fitCompact}>Compact</button></div>
        {!selected?<p className="muted">Click a card to see position details.</p>:<>
          <div className="studio-detail-head"><OrgAvatar name={selected.full_name||selected.position_title} path={selected.photo_path}/><div><b>{selected.position_title}</b><span>{selected.full_name||selected.position_status}</span></div></div>
          <dl>
            <dt>Position Code</dt><dd>{selected.position_code}</dd>
            <dt>Status</dt><dd>{selected.position_status}</dd>
            <dt>Org Unit</dt><dd>{selected.org_unit_name||"—"}</dd>
            <dt>Site</dt><dd>{selected.site_code||"—"}</dd>
            <dt>Band / Grade</dt><dd>{selected.band||"—"} / {selected.grade||"—"}</dd>
            <dt>Parent</dt><dd>{selected.parent_position_title||"—"}</dd>
          </dl>
          <div className="toolbar"><button className="btn ghost small" onClick={()=>openEditPosition(selected)}>Edit</button><button className="btn ghost small" onClick={()=>promptQuickAdd(selected,"below")}>Add Below</button><button className="btn ghost small" onClick={()=>promptQuickAdd(selected,"beside")}>Add Beside</button><button className="btn ghost small" onClick={()=>promptQuickAdd(selected,"above")}>Add Above</button><button className="btn danger small" onClick={()=>deactivatePosition(selected)}>Deactivate</button></div>
        </>}
      </div>
    </div>}

    {tab==="explorer"&&<div className="studio-workspace single"><div className="studio-canvas card"><div className="studio-canvas-tools"><button onClick={fitCompact}>Compact Fit</button><button onClick={resetCanvas}>Reset</button><span className="studio-nav-tip">Drag empty canvas to pan</span></div><div className="studio-pan-surface" onMouseDown={handleCanvasMouseDown} onMouseMove={handleCanvasMouseMove} onMouseUp={()=>setDragging(null)} onMouseLeave={()=>setDragging(null)} onWheel={handleCanvasWheel}><div className={cn("studio-tree",density,dragging&&"dragging")} style={{transform:`translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,transformOrigin:"top center"}}>{visibleRoots().map(r=>card(r))}</div></div></div></div>}

    {tab==="positions"&&<div className="table card studio-table"><table><thead><tr><th>Position</th><th>Occupant</th><th>Status</th><th>Parent</th><th>Org Unit</th><th>Site</th><th>Band</th><th>Grade</th><th>Actions</th></tr></thead><tbody>{nodesAll.filter(filterNode).map(n=><tr key={n.position_id}><td><b>{n.position_title}</b><br/><span>{n.position_code}</span></td><td>{n.full_name||"—"}</td><td><span className={cn("badge",orgStatusBadge(n.position_status))}>{n.position_status}</span></td><td>{n.parent_position_title||"—"}</td><td>{n.org_unit_name||"—"}</td><td>{n.site_code||"—"}</td><td>{n.band||"—"}</td><td>{n.grade||"—"}</td><td><button className="btn ghost small" onClick={()=>setSelected(n)}>Details</button></td></tr>)}</tbody></table></div>}

    {tab==="vacancies"&&<div className="table card studio-table"><table><thead><tr><th>Position</th><th>Status</th><th>Criticality</th><th>Hiring</th><th>Target DOJ</th><th>Org Unit</th><th>Site</th></tr></thead><tbody>{nodesAll.filter(n=>["vacant","planned","frozen"].includes(n.position_status)).map(n=><tr key={n.position_id}><td><b>{n.position_title}</b><br/><span>{n.position_code}</span></td><td><span className={cn("badge",orgStatusBadge(n.position_status))}>{n.position_status}</span></td><td>{n.criticality}</td><td>{n.hiring_status||"—"}</td><td>{n.target_joining_date||"—"}</td><td>{n.org_unit_name||"—"}</td><td>{n.site_code||"—"}</td></tr>)}</tbody></table></div>}

    {tab==="gaps"&&<div className="table card studio-table"><table><thead><tr><th>Position</th><th>Employee</th><th>Gaps</th></tr></thead><tbody>{gaps.map(g=><tr key={g.position_id}><td>{g.position_title}<br/><span>{g.position_code}</span></td><td>{g.full_name||"—"}</td><td>{(g.gaps||[]).map(x=><span className="badge amber" key={x}>{x}</span>)}</td></tr>)}</tbody></table></div>}

    {tab==="versions"&&<div className="table card studio-table"><table><thead><tr><th>Version</th><th>Status</th><th>Purpose</th><th>Design Principle</th><th>Created</th></tr></thead><tbody>{versions.map(v=><tr key={v.id}><td><b>{v.version_name}</b><br/><span>{v.version_code}</span></td><td><span className="badge blue">{v.status}</span></td><td>{v.purpose||"—"}</td><td>{v.design_principle||"—"}</td><td>{new Date(v.created_at).toLocaleString()}</td></tr>)}</tbody></table></div>}

    {tab==="exports"&&<div className="card premium studio-export">
      <h3>Exports / Review Pack</h3>
      <p className="muted">Use these for leadership reviews and PowerPoint/SmartArt creation.</p>
      <div className="toolbar"><button className="btn gold" onClick={exportHierarchy}>Copy PowerPoint hierarchy text</button><button className="btn ghost" onClick={exportCsv}>Download position schema CSV</button><button className="btn ghost" onClick={()=>copyTextToClipboard(JSON.stringify({version:versions.find(v=>v.id===activeVersionId),nodes:nodesAll,edges},null,2))}>Copy JSON schema</button></div>
      <pre className="studio-pre">{makeHierarchyText(view==="people"?"people":"position")}</pre>
    </div>}

    {tab==="debug"&&<div className="card premium">
      <h3>Debug Center</h3>
      <p className="muted">Use this if the module appears blank or counts look wrong.</p>
      <div className="studio-metrics"><div><b>{versions.length}</b><span>Versions</span></div><div><b>{nodesAll.length}</b><span>Visible Nodes</span></div><div><b>{edges.length}</b><span>Edges</span></div><div><b>{loadWarnings.length}</b><span>Warnings</span></div></div>
      <button className="btn ghost" onClick={exportDebug}>Download Debug CSV</button>
      <pre className="studio-pre">{JSON.stringify({activeVersionId,loadWarnings,summary,orgMode,derivedRootCount,mainRoot:mainRoot&&{position:mainRoot.position_title,code:mainRoot.position_code,descendants:countDescendants(mainRoot)},orphanRootCount:orphanRoots.length,edgeSample:edges.slice(0,3),rootSample:(childMap.ROOT||[]).slice(0,8).map(n=>({position:n.position_title,code:n.position_code,id:n.position_id,descendants:countDescendants(n)}))},null,2)}</pre>
    </div>}

    {editOpen&&<div className="modal-backdrop" onClick={()=>setEditOpen(false)}>
      <div className="modal-card org-edit-modal" onClick={e=>e.stopPropagation()}>
        <h3>Edit Position</h3>
        <label>Position Title<input value={editDraft.position_title||""} onChange={e=>setEditDraft({...editDraft,position_title:e.target.value})}/></label>
        <label>Status<select value={editDraft.position_status||"occupied"} onChange={e=>setEditDraft({...editDraft,position_status:e.target.value})}>{["occupied","vacant","planned","frozen","inactive"].map(x=><option key={x} value={x}>{humanLabel(x)}</option>)}</select></label>
        <label>Band<input value={editDraft.band||""} onChange={e=>setEditDraft({...editDraft,band:e.target.value})}/></label>
        <label>Grade<input value={editDraft.grade||""} onChange={e=>setEditDraft({...editDraft,grade:e.target.value})}/></label>
        <label>Designation Track<input value={editDraft.designation_track||""} onChange={e=>setEditDraft({...editDraft,designation_track:e.target.value})}/></label>
        <label>Notes<textarea value={editDraft.notes||""} onChange={e=>setEditDraft({...editDraft,notes:e.target.value})}/></label>
        <div className="toolbar"><button className="btn gold" onClick={saveEditPosition}>Save</button><button className="btn ghost" onClick={()=>setEditOpen(false)}>Cancel</button><button className="btn ghost" onClick={showCrudRoadmap}>CRUD Roadmap</button></div>
      </div>
    </div>}

    {ctx&&<div className="studio-context" style={{left:ctx.x,top:ctx.y}} onClick={e=>e.stopPropagation()}>
      <b>{ctx.node.position_title}</b>
      <button onClick={()=>openEditPosition(ctx.node)}>Edit Position</button>
      <button onClick={()=>quickAdd(ctx.node,"above")}>Add Position Above</button>
      <button onClick={()=>quickAdd(ctx.node,"below")}>Add Position Below</button>
      <button onClick={()=>quickAdd(ctx.node,"beside")}>Add Position Beside</button>
      <button onClick={()=>detachFromParent(ctx.node)}>Detach from Parent</button>
      <button onClick={()=>focusBranch(ctx.node)}>Focus Branch</button>
      <button onClick={()=>{setSelected(ctx.node);setCtx(null)}}>Open Details</button>
      <button onClick={()=>deactivatePosition(ctx.node)}>Deactivate Position</button>
      <button onClick={()=>{copyTextToClipboard(ctx.node.position_code);setCtx(null)}}>Copy Position Code</button>
    </div>}
  </div>
}

function OrgChartRoute(){return <OrgChartErrorBoundary><OrgChartModule/></OrgChartErrorBoundary>}

function OrgChartModule(){
  const {data,loading,error,reload}=useLoad(loadOrgChartData,[]);
  const [tab,setTab]=useState("chart");
  const [q,setQ]=useState("");
  const [selected,setSelected]=useState(null);
  const [form,setForm]=useState({});
  const [scale,setScale]=useState(1);
  const [layout,setLayout]=useState("vertical");

  if(loading)return <div className="empty">Loading org chart...</div>;
  if(error)return <div className="card premium"><p className="error">{error}</p><p className="muted">Run SQL 22 first.</p></div>;

  const nodes=data.nodes||[];
  const edges=data.edges||[];
  const orgs=data.orgs||[];
  const gaps=data.gaps||[];
  const logs=data.logs||[];
  const loadWarnings=data.loadWarnings||[];

  const filtered=nodes.filter(n=>{
    const hay=[n.employee_code,n.full_name,n.designation_name,n.org_unit_name,n.reporting_manager_name,n.department_name].join(" ").toLowerCase();
    return hay.includes(q.toLowerCase());
  });

  const byEmp=Object.fromEntries(nodes.map(n=>[n.employee_id,n]));
  const childrenByMgr={};
  nodes.forEach(n=>{
    const key=n.reporting_manager_id||"ROOT";
    childrenByMgr[key]=childrenByMgr[key]||[];
    childrenByMgr[key].push(n);
  });
  Object.values(childrenByMgr).forEach(arr=>arr.sort((a,b)=>String(a.employee_code||"").localeCompare(String(b.employee_code||""),undefined,{numeric:true})));

  function openEdit(n){
    setSelected(n);
    setForm({
      org_unit_code:n.org_unit_code||"",
      reporting_manager_code:n.reporting_manager_code||"",
      functional_head_code:n.functional_head_code||"",
      hr_partner_code:n.hr_partner_code||"",
      effective_from:new Date().toISOString().slice(0,10),
      reason:"Org chart update"
    });
    setTab("edit");
  }

  async function saveMapping(){
    if(!selected)return;
    try{
      const {error}=await supabase.rpc("rpc_update_employee_org_mapping",{
        p_employee_id:selected.employee_id,
        p_org_unit_code:form.org_unit_code||null,
        p_reporting_manager_code:form.reporting_manager_code||null,
        p_functional_head_code:form.functional_head_code||null,
        p_hr_partner_code:form.hr_partner_code||null,
        p_effective_from:form.effective_from||null,
        p_reason:form.reason||null
      });
      if(error)throw error;
      setSelected(null);setForm({});setTab("people");reload();
    }catch(e){console.error(e.message||String(e));}
  }

  const initials=(n)=>init(n.full_name||n)
  function nodeCard(n,depth=0){
    const childCount=(childrenByMgr[n.employee_id]||[]).length;
    return <div className="org-node-wrap" key={n.employee_id}>
      <div className={cn("org-person-card",n.mapping_quality!=="ok"&&"warn")} onClick={()=>openEdit(n)}>
        <OrgAvatar name={n.full_name} path={n.photo_path} compact={true}/>
        <div className="org-person-info">
          <b>{n.full_name}</b>
          <span>{n.employee_code} · {n.designation_name||"Designation missing"}</span>
          <em>{n.org_unit_name||"Org unit missing"} {n.grade?`· ${n.grade}`:""}</em>
        </div>
        <div className="org-person-badges">
          {childCount>0&&<span>{childCount} report(s)</span>}
          {n.mapping_quality!=="ok"&&<span className="bad">Gap</span>}
        </div>
      </div>
      {(childrenByMgr[n.employee_id]||[]).length>0&&<div className="org-children">{childrenByMgr[n.employee_id].map(c=>nodeCard(c,depth+1))}</div>}
    </div>
  }

  const roots=(childrenByMgr.ROOT||[]).length ? childrenByMgr.ROOT : nodes.filter(n=>!n.reporting_manager_id).slice(0,50);

  function downloadOrgCsv(){
    const rows=nodes.map(n=>({
      employee_code:n.employee_code, full_name:n.full_name, designation:n.designation_name,
      org_unit_code:n.org_unit_code, org_unit_name:n.org_unit_name, org_unit_type:n.org_unit_type,
      reporting_manager_code:n.reporting_manager_code, reporting_manager_name:n.reporting_manager_name,
      functional_head_code:n.functional_head_code, functional_head_name:n.functional_head_name,
      hr_partner_code:n.hr_partner_code, hr_partner_name:n.hr_partner_name,
      mapping_quality:n.mapping_quality
    }));
    downloadCsv("org_chart_nodes.csv",rows);
  }

  return <div className="org-module">
    <div className="org-hero card premium">
      <div>
        <h2>Org Chart</h2>
        <p>Manage structure, reporting relationships and employee org mapping from one module.</p>{loadWarnings.length>0&&<p className="error">Load warning: {loadWarnings[0]}</p>}{nodes.length===0&&<p className="error">No visible org nodes. Run SQL 22D view visibility fix, then refresh.</p>}
      </div>
      <div className="org-metrics">
        <div><b>{nodes.length}</b><span>People</span></div>
        <div><b>{edges.length}</b><span>Links</span></div>
        <div><b>{gaps.length}</b><span>Gaps</span></div>
        <div><b>{orgs.length}</b><span>Org Nodes</span></div>
      </div>
    </div>

    <div className="org-tabs">
      {["chart","people","gaps","edit","audit"].map(t=><button key={t} className={cn(tab===t&&"active")} onClick={()=>setTab(t)}>{humanLabel(t)}</button>)}
      <input className="input" placeholder="Search employee, code, org unit, manager..." value={q} onChange={e=>setQ(e.target.value)}/>
      <button className="btn ghost" onClick={downloadOrgCsv}>Download CSV</button>
      <button className="btn ghost" onClick={reload}>Refresh</button>
    </div>

    {tab==="chart"&&<div className="org-chart-shell card">
      {nodes.length===0&&<div className="empty"><b>No org chart rows visible yet.</b><br/>Run SQL 22D, then refresh. If this still appears, check RLS/tenant mapping for employees and assignments.</div>}
      <div className="toolbar">
        <button className="btn ghost" onClick={()=>setScale(Math.max(.6,scale-.1))}>−</button>
        <span className="muted">{Math.round(scale*100)}%</span>
        <button className="btn ghost" onClick={()=>setScale(Math.min(1.5,scale+.1))}>+</button>
        <button className="btn ghost" onClick={()=>setLayout(layout==="vertical"?"compact":"vertical")}>{layout==="vertical"?"Compact":"Vertical"}</button>
      </div>
      <div className={cn("org-tree-canvas",layout==="compact"&&"compact")} style={{transform:`scale(${scale})`,transformOrigin:"top center"}}>
        {roots.map(r=>nodeCard(r))}
      </div>
    </div>}

    {tab==="people"&&<div className="table card org-table">{filtered.length===0&&<div className="empty">No people rows found. Check SQL 22D verification and tenant access.</div>}<table><thead><tr>
      <th>Code</th><th>Name</th><th>Designation</th><th>Org Unit</th><th>Manager</th><th>Functional Head</th><th>HR Partner</th><th>Quality</th><th>Action</th>
    </tr></thead><tbody>{filtered.map(n=><tr key={n.employee_id}>
      <td>{n.employee_code}</td><td>{n.full_name}</td><td>{n.designation_name||"—"}</td><td>{n.org_unit_name||"—"}</td><td>{n.reporting_manager_name||"—"}</td><td>{n.functional_head_name||"—"}</td><td>{n.hr_partner_name||"—"}</td><td><span className={cn("badge",n.mapping_quality==="ok"?"green":"amber")}>{n.mapping_quality}</span></td><td><button className="btn ghost small" onClick={()=>openEdit(n)}>Edit Mapping</button></td>
    </tr>)}</tbody></table></div>}

    {tab==="gaps"&&<div className="table card org-table">{gaps.length===0&&<div className="empty">No gaps found or gaps view not visible.</div>}<table><thead><tr><th>Code</th><th>Name</th><th>Gaps</th><th>Action</th></tr></thead><tbody>{gaps.map(g=><tr key={g.employee_id}><td>{g.employee_code}</td><td>{g.full_name}</td><td>{(g.gaps||[]).map(x=><span className="badge amber" key={x}>{x}</span>)}</td><td><button className="btn ghost small" onClick={()=>openEdit(nodes.find(n=>n.employee_id===g.employee_id))}>Fix</button></td></tr>)}</tbody></table></div>}

    {tab==="edit"&&<div className="card premium org-edit">
      <div className="cardTitle"><div><h3>{selected?`Edit Org Mapping — ${selected.full_name}`:"Select an employee"}</h3><p className="muted">Changes here update org assignment history and helper fields in Employee Master.</p></div></div>
      {!selected?<div className="empty">Select an employee from People or Chart.</div>:<div className="formGrid emp-form-grid">
        <SmartSelect field="org_unit_code" label="Org Unit" value={form.org_unit_code} onChange={v=>setForm({...form,org_unit_code:v})} options={orgs.map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))}/>
        <SmartSelect field="reporting_manager_code" label="Reporting Manager" value={form.reporting_manager_code} onChange={v=>setForm({...form,reporting_manager_code:v})} options={nodes.filter(n=>n.employee_id!==selected.employee_id).map(n=>({...n,code:n.employee_code,label:n.full_name,pathLabel:`${n.designation_name||""} · ${n.org_unit_name||""}`}))}/>
        <SmartSelect field="functional_head_code" label="Functional Head" value={form.functional_head_code} onChange={v=>setForm({...form,functional_head_code:v})} options={nodes.filter(n=>n.employee_id!==selected.employee_id).map(n=>({...n,code:n.employee_code,label:n.full_name,pathLabel:`${n.designation_name||""} · ${n.org_unit_name||""}`}))}/>
        <SmartSelect field="hr_partner_code" label="HR Partner" value={form.hr_partner_code} onChange={v=>setForm({...form,hr_partner_code:v})} options={nodes.filter(n=>n.employee_id!==selected.employee_id).map(n=>({...n,code:n.employee_code,label:n.full_name,pathLabel:`${n.designation_name||""} · ${n.org_unit_name||""}`}))}/>
        <TextInput field="effective_from" label="Effective From" type="date" value={form.effective_from} onChange={v=>setForm({...form,effective_from:v})}/>
        <TextInput field="reason" label="Reason / Note" value={form.reason} onChange={v=>setForm({...form,reason:v})}/>
        <div className="toolbar"><button className="btn gold" onClick={saveMapping}>Save Mapping</button><button className="btn ghost" onClick={()=>{setSelected(null);setTab("people")}}>Cancel</button></div>
      </div>}
    </div>}

    {tab==="audit"&&<div className="table card org-table"><table><thead><tr><th>When</th><th>Change</th><th>Employee</th><th>Field</th><th>Old</th><th>New</th><th>Reason</th></tr></thead><tbody>{logs.map(l=><tr key={l.id}><td>{new Date(l.created_at).toLocaleString()}</td><td>{l.change_type}</td><td>{nodes.find(n=>n.employee_id===l.employee_id)?.full_name||l.employee_id}</td><td>{l.field_name}</td><td>{l.old_value}</td><td>{l.new_value}</td><td>{l.reason}</td></tr>)}</tbody></table></div>}
  </div>
}

function Guard({page,profile,roles}){
  const map={punch:<GuardPunch profile={profile} roles={roles}/>,inside:<GuardInside/>,visitors:<GuardVisitors profile={profile}/>,lookup:<GuardLookup/>,manual:<GuardManual profile={profile}/>,emergency:<GuardEmergency/>,handover:<GuardHandover profile={profile}/>};
  return map[page]||map.punch;
}
function GuardPunch({profile,roles}){
  useEffect(()=>{ensureGuardConsoleCss();},[]);
  const [codeQ,setCodeQ]=useState("");
  const [nameQ,setNameQ]=useState("");
  const [selected,setSelected]=useState(null);
  const [sessionPunches,setSessionPunches]=useState([]);
  const [guardName,setGuardName]=useState(localStorage.getItem("guardName")||"");
  const [guardSiteId,setGuardSiteId]=useState(localStorage.getItem("guardSiteId")||"");
  const [actionBusy,setActionBusy]=useState(false);
  const [guardErr,setGuardErr]=useState("");
  const [manualModal,setManualModal]=useState(false);
  const [sendModal,setSendModal]=useState(false);
  const [manualForm,setManualForm]=useState({action:"FORCE_IN",reason:""});
  const [contextTab,setContextTab]=useState("recent");
  const [activeSuggestion,setActiveSuggestion]=useState(0);
  const [tick,setTick]=useState(Date.now());
  const [overrideOpen,setOverrideOpen]=useState(false);

  useEffect(()=>{
    const id=setInterval(()=>setTick(Date.now()),60000);
    return ()=>clearInterval(id);
  },[]);

  const {data,loading,error,reload}=useLoad(async()=>{
    const [empRes,insideRes,visRes,eventRes,regRes,sitesRes,transitRes,stateRes]=await Promise.all([
      supabase.from("employees").select("*").in("status",["active"]).order("employee_code").limit(2000),
      supabase.from("v_people_inside_now").select("*").order("last_event_time",{ascending:false}),
      supabase.from("v_active_visitors").select("*").order("entry_time",{ascending:false}),
      supabase.from("attendance_events").select("*").order("event_time",{ascending:false}).limit(20),
      supabase.from("v_pending_attendance_approvals").select("*"),
      supabase.from("sites").select("*").eq("is_active",true).order("site_code"),
      supabase.from("v_employee_site_transits_active").select("*").order("departed_at",{ascending:false}),
      supabase.from("v_gate_console_state").select("*")
    ]);

    if(empRes.error)throw empRes.error;
    if(insideRes.error)throw insideRes.error;
    if(visRes.error)throw visRes.error;
    if(eventRes.error)throw eventRes.error;
    if(sitesRes.error)throw sitesRes.error;
    if(transitRes.error)throw new Error("Transit table/view missing. Run SQL 07 first: " + transitRes.error.message);
    if(stateRes.error)throw new Error("Movement state view missing. Run SQL 07/12 first: " + stateRes.error.message);

    const maps=await getOrgMaps();
    const employees=(empRes.data||[])
      .map(e=>withOrg(e,maps))
      .sort((a,b)=>codeNum(a.employee_code)-codeNum(b.employee_code) || String(a.full_name||"").localeCompare(String(b.full_name||"")));

    const ids=[...new Set([
      ...(insideRes.data||[]).map(x=>x.employee_id),
      ...(eventRes.data||[]).map(x=>x.employee_id),
      ...(transitRes.data||[]).map(x=>x.employee_id)
    ].filter(Boolean))];

    let empMap={};
    if(ids.length){
      const {data:empRows,error:empErr}=await supabase.from("employees").select("*").in("id",ids);
      if(empErr)throw empErr;
      empMap=Object.fromEntries((empRows||[]).map(e=>[e.id,withOrg(e,maps)]));
    }

    const inside=(insideRes.data||[]).map(x=>({...x, photo_path:empMap[x.employee_id]?.photo_path}));
    const insideIds=new Set(inside.map(x=>x.employee_id));
    const events=(eventRes.data||[]).map(x=>({...x, employee:empMap[x.employee_id]}));
    const transit=(transitRes.data||[]).map(x=>({...x, employee:empMap[x.employee_id]}));
    const movementStates=Object.fromEntries((stateRes.data||[]).map(x=>[x.employee_id,x]));

    return {
      employees,
      inside,
      visitors:visRes.data||[],
      events,
      insideIds,
      pendingApprovals:regRes.data||[],
      sites:sitesRes.data||[],
      transits:transit,
      movementStates
    };
  },[]);

  const employees=data?.employees||[];
  const inside=data?.inside||[];
  const visitors=data?.visitors||[];
  const events=data?.events||[];
  const insideIds=data?.insideIds||new Set();
  const pendingApprovals=data?.pendingApprovals||[];
  const sites=data?.sites||[];
  const transits=data?.transits||[];
  const movementStates=data?.movementStates||{};
  const guardSite=sites.find(s=>s.id===guardSiteId)||null;

  const queryCode=codeQ.trim().toLowerCase();
  const queryName=nameQ.trim().toLowerCase();
  const hasSearch=!!(queryCode||queryName);

  const selectedTransit = selected ? transits.find(t=>t.employee_id===selected.id) : null;
  const selectedPresence=selected ? inside.find(x=>x.employee_id===selected.id) : null;
  const selectedInside=selected ? insideIds.has(selected.id) : false;
  const selectedSiteId = selectedPresence?.site_id || selected?.site_id || null;
  const selectedSiteCode = selectedPresence?.site_code || selected?.site_code || "";

  function stateOf(emp){
    if(!emp) return {state:"NONE", label:"Select employee", allowed:{in:false,out:false,break:false,return:false,transfer:false,receive:false,officialOut:false,officialReturn:false,breakToOut:false,transitOut:false,manual:true}, message:"Select an employee."};
    const mv=movementStates[emp.id] || {};
    const dbState=mv.current_state || "OUTSIDE";
    const baseAllowed={in:false,out:false,break:false,return:false,transfer:false,receive:false,officialOut:false,officialReturn:false,breakToOut:false,transitOut:false,manual:true};
    const common={dayFirstIn:mv.day_first_in_at, currentSince:mv.current_location_since_at, mobile:mv.personal_mobile, mv};

    if(dbState==="ACCESS_BLOCKED"){
      return {...common,state:"ACCESS_BLOCKED",label:"Access Blocked",allowed:baseAllowed,message:"Employee is inactive, exited, or gate access is disabled."};
    }
    if(dbState==="IN_TRANSIT"){
      const mins=mv.running_transit_minutes ?? (mv.transit_started_at ? minutesSince(mv.transit_started_at) : 0);
      return {...common,state:"IN_TRANSIT",label:`In transit ${mv.from_site_code||""} → ${mv.to_site_code||""}`,allowed:{...baseAllowed,receive:true,transitOut:true},attention:mv.transit_attention_level,message:`Day IN: ${mv.day_first_in_at?timeOnly(mv.day_first_in_at):"—"} · Transit running ${minutesLabel(mins)}. Destination receive is normal; source/third-site receive is exception with audit.`};
    }
    if(dbState==="ON_BREAK"){
      const mins=mv.running_break_minutes ?? (mv.last_event_time ? minutesSince(mv.last_event_time) : 0);
      return {...common,state:"ON_BREAK",label:`On Break ${mv.current_site_code||""}`,allowed:{...baseAllowed,return:true,breakToOut:true},attention:mv.break_attention_level,message:`Day IN: ${mv.day_first_in_at?timeOnly(mv.day_first_in_at):"—"} · Break running ${minutesLabel(mins)}. Can return, or close as Break → OUT with reason.`};
    }
    if(dbState==="ON_OFFICIAL_DUTY"){
      const mins=mv.running_official_duty_minutes ?? (mv.last_event_time ? minutesSince(mv.last_event_time) : 0);
      return {...common,state:"ON_OFFICIAL_DUTY",label:`Official Duty Out ${mv.current_site_code||""}`,allowed:{...baseAllowed,officialReturn:true},message:`Day IN: ${mv.day_first_in_at?timeOnly(mv.day_first_in_at):"—"} · Official duty out for ${minutesLabel(mins)}.`};
    }
    if(dbState==="INSIDE"){
      return {...common,state:"INSIDE",label:`Inside ${mv.current_site_code||""}`,allowed:{...baseAllowed,out:true,break:true,transfer:true,officialOut:true},message:`Day IN: ${mv.day_first_in_at?timeOnly(mv.day_first_in_at):"—"} · Current location since ${mv.current_location_since_at?timeOnly(mv.current_location_since_at):"—"}.`};
    }
    return {...common,state:"OUTSIDE",label:"Outside",allowed:{...baseAllowed,in:true},message:"Employee is currently outside. Mark IN at the guard site where they are entering."};
  }

  const selectedState = stateOf(selected);

  const suggestions=employees
    .map(e=>{
      const code=String(e.employee_code||"").toLowerCase();
      const name=String(e.full_name||"").toLowerCase();
      const meta=personMeta(e).toLowerCase();
      let score=1000;
      if(queryCode && code===queryCode) score=0;
      else if(queryCode && code.startsWith(queryCode)) score=5;
      else if(queryName && name.startsWith(queryName)) score=10;
      else if(queryName && name.includes(queryName)) score=25;
      else if(queryCode && code.includes(queryCode)) score=30;
      else if((queryCode && meta.includes(queryCode)) || (queryName && meta.includes(queryName))) score=50;
      const st=stateOf(e);
      return {...e,_score:score,_inside:insideIds.has(e.id),_stateLabel:st.label,_state:st.state};
    })
    .filter(e=>hasSearch ? e._score<1000 : false)
    .sort((a,b)=>a._score-b._score || codeNum(a.employee_code)-codeNum(b.employee_code) || String(a.full_name||"").localeCompare(String(b.full_name||"")))
    .slice(0,8);

  useEffect(()=>setActiveSuggestion(0),[codeQ,nameQ]);

  function choose(emp){
    setSelected(emp);
    setCodeQ(String(emp.employee_code||""));
    setNameQ(String(emp.full_name||""));
  }

  function clearSearch(){
    setCodeQ("");
    setNameQ("");
    setSelected(null);
  }

  function onSearchKeyDown(e){
    if(!suggestions.length)return;
    if(e.key==="ArrowDown"){e.preventDefault();setActiveSuggestion(i=>Math.min(i+1,suggestions.length-1))}
    if(e.key==="ArrowUp"){e.preventDefault();setActiveSuggestion(i=>Math.max(i-1,0))}
    if(e.key==="Enter"){e.preventDefault();choose(suggestions[activeSuggestion]||suggestions[0])}
    if(e.key==="Escape"){e.preventDefault();clearSearch()}
  }

  function blockIfNotAllowed(actionKey){
    if(!selected){setGuardErr("Select an employee first.");return true}
    if(!selectedState.allowed[actionKey]){
      setGuardErr(`${humanLabel(actionKey)} not allowed now. Status: ${selectedState.label}. Use Manual Override for corrections.`);
      return true;
    }
    setGuardErr(""); return false;
  }

  async function gateAction(action, opts={}){
    if(!selected){setGuardErr("Select an employee first.");return}
    if(actionBusy)return;
    if(!guardSiteId){setGuardErr("Select your current guard site first.");return}
    const remarks = opts.remarks || null;
    setActionBusy(true);
    const {error,data}=await supabase.rpc("rpc_gate_action",{
      p_employee_id:selected.id,
      p_action:action,
      p_guard_site_id:guardSiteId,
      p_target_site_id:opts.targetSiteId || null,
      p_event_time:null,
      p_guard_name:guardName || null,
      p_remarks:remarks,
      p_client_request_id:`gate-${action}-${crypto.randomUUID()}`
    });
    setActionBusy(false);
    if(error){setGuardErr(error.message||String(error)); reload(); return}
    setSessionPunches([{emp:selected,type:opts.label||action,time:new Date()},...sessionPunches].slice(0,10));
    reload();
  }

  async function punch(emp,eventType,reasonCode,label,permissionKey){
    const map={in:"IN",out:"FINAL_OUT",break:"BREAK_OUT",return:"RETURN_FROM_BREAK",officialOut:"OFFICIAL_DUTY_OUT",officialReturn:"OFFICIAL_DUTY_RETURN",breakToOut:"BREAK_TO_OUT"};
    return gateAction(map[permissionKey]||"IN",{label,askReason:permissionKey==="breakToOut"||permissionKey==="officialOut",prompt:permissionKey==="breakToOut"?"Reason for Break → OUT:":"Reason / destination for Official Duty Out:"});
  }

  async function startTransfer(){
    if(actionBusy)return;
    if(!selected){setGuardErr("Select an employee first.");return}
    if(!guardSiteId){setGuardErr("Select your guard site first.");return}
    if(!selectedState.allowed.transfer){setGuardErr(selectedState.message || "Send to Site not allowed in current state.");return}
    const otherSites = sites.filter(s=>s.id !== guardSiteId);
    // site selection now via modal
    const dest = otherSites.find(s=>String(s.site_code).toLowerCase()===String(destCode).trim().toLowerCase() || String(s.site_name).toLowerCase()===String(destCode).trim().toLowerCase());
    if(!dest){setGuardErr("Destination site not found.");return}
    return gateAction("SEND_TO_SITE",{targetSiteId:dest.id,label:`Send to ${dest.site_code||dest.site_name}`,remarks:`Sent to ${dest.site_code||dest.site_name}`});
  }

  async function receiveTransit(transitRow){
    const row = transitRow || selectedTransit || selectedState?.transit;
    if(!row && selectedState.state!=="IN_TRANSIT"){setGuardErr("No active transit selected.");return}
    if(!selected){ 
      const empFromRow = row ? employees.find(e=>e.id===row.employee_id) : null;
      if(empFromRow) setSelected(empFromRow);
    }
    if(!guardSiteId){setGuardErr("Select your current guard site first.");return}
    const isDest = row?.to_site_id ? guardSiteId===row.to_site_id : true;
    const isSource = row?.from_site_id ? guardSiteId===row.from_site_id : false;
    let remarks = null;
    if(!isDest){
      remarks = "Third-site / source receive — exception logged automatically";
    }
    if(row && !selected){
      setGuardErr("Select the employee from search first, then use Receive In.");
      return;
    }
    return gateAction("RECEIVE_IN",{label:isDest?`Received at ${row?.to_site_code||"site"}`:(isSource?"Returned to Source":"Third-site Receive"),remarks});
  }

  async function submitManualOverride(overrideAction,label,reason){
    if(!selected||!overrideAction)return;
    const {error}=await supabase.rpc("rpc_guard_manual_override",{
      p_employee_id:selected.id,
      p_override_action:overrideAction,
      p_site_id:selectedSiteId || selected.site_id || null,
      p_guard_name:guardName || null,
      p_reason:reason||"Manual override by guard",
      p_client_request_id:`override-${crypto.randomUUID()}`
    });
    if(error){setGuardErr(error.message);}else{
      setGuardErr("");
      setSessionPunches([{emp:selected,type:label,time:new Date()},...sessionPunches].slice(0,10));
      setManualModal(false);
      reload();
    }
  }

  const p77=inside.filter(x=>x.site_code==="P77").length;
  const p79=inside.filter(x=>x.site_code==="P79").length;
  const overstay=visitors.filter(v=>v.overstay_flag).length;
  const recent=[...sessionPunches.map(x=>({id:`local-${x.time}`, employee:x.emp, event_type:x.type, event_time:x.time, local:true})),...events].slice(0,10);
  const alerts=[
    ...visitors.filter(v=>v.overstay_flag).map(v=>({kind:"Overstay",tone:"amber",title:v.visitor_name,body:`Inside ${runTime(v.entry_time)} · To meet ${v.host_employee_name||v.host_name||"—"}`})),
    ...transits.map(t=>({kind:"In Transit",tone:"goldb",title:`${t.employee_code} — ${t.full_name}`,body:`${t.from_site_code||"—"} → ${t.to_site_code||"—"} · ${minutesToRunTime(t.running_transit_minutes)}`})),
    ...pendingApprovals.map(a=>({kind:"Approval",tone:"goldb",title:`${a.employee_code} — ${a.full_name}`,body:`${a.request_type} · ${a.work_date}`}))
  ];

  if(error)return <div className="cp-console-error">{error}</div>;

  return <div className="cp-guard-console">
    <div className="cp-gc-kpis">
      <div className="cp-gc-kpi"><span>Total On-Site</span><b>{inside.length+visitors.length}</b></div>
      <div className="cp-gc-kpi blue"><span>Active Master</span><b>{employees.length}</b></div>
      <div className="cp-gc-kpi teal"><span>Employees</span><b>{inside.length}</b></div>
      <div className="cp-gc-kpi amber"><span>Visitors</span><b>{visitors.length}</b></div>
      <div className="cp-gc-kpi amber"><span>In Transit</span><b>{transits.length}</b></div>
      <div className="cp-gc-kpi"><span>Plot 77</span><b>{p77}</b></div>
      <div className="cp-gc-kpi"><span>Plot 79</span><b>{p79}</b></div>
      <button className="cp-gc-kpi cp-refresh-kpi" onClick={reload}><span>Reload</span><b>↻</b></button>
    </div>

    <div className="cp-gc-head">
      <div>
        <h2>Gate Console</h2>
        <p>Search, verify, punch, track visitors and inter-site movement from one screen.</p>
      </div>
      <div className="cp-duty cp-duty-grid">
        <div><label>On Duty Guard</label>
        <input value={guardName} onChange={e=>{setGuardName(e.target.value);localStorage.setItem("guardName",e.target.value)}} placeholder="Guard name"/></div>
        <div><label>Current Guard Site</label>
        <select value={guardSiteId} onChange={e=>{setGuardSiteId(e.target.value);localStorage.setItem("guardSiteId",e.target.value)}}>
          <option value="">Select site</option>
          {sites.map(s=><option key={s.id} value={s.id}>{s.site_code||s.site_name} — {s.site_name||s.site_code}</option>)}
        </select></div>
      </div>
    </div>

    <div className="cp-gc-layout">
      <section className="cp-card cp-punch-card">
        <div className="cp-card-title">
          <div><b>Employee Punch</b><span>State-aware guardrails + inter-site transfer</span></div>
          <em>{selectedState.label}</em>
        </div>

        <div className="cp-search-two">
          <div>
            <label>Employee Code</label>
            <input inputMode="numeric" placeholder="e.g. 104" value={codeQ} onChange={e=>{setCodeQ(e.target.value); if(selected)setSelected(null)}} onKeyDown={onSearchKeyDown}/>
          </div>
          <div>
            <label>Employee Name</label>
            <input placeholder="e.g. Amit Kumar" value={nameQ} onChange={e=>{setNameQ(e.target.value); if(selected)setSelected(null)}} onKeyDown={onSearchKeyDown}/>
          </div>
        </div>

        <div className="cp-search-meta">
          <button onClick={clearSearch}>Clear</button>
          <span>{loading?"Loading employees…":hasSearch?`${suggestions.length} match(es)`:`${employees.length} active employees loaded from Employee Master`}</span>
        </div>
        {!selected && <div className="cp-search-hint">Select an employee from suggestions. Photo/initials and status are shown in each row.</div>}

        {selected && <div className="cp-selected cp-selected-compact">
          <Avatar name={selected.full_name} path={selected.photo_path}/>
          <div className="cp-selected-main">
            <h3>{selected.full_name}</h3>
            <p>E-Code <b>{selected.employee_code}</b>{selectedPresence ? <> · Inside <b>{runTime(selectedPresence.last_event_time)}</b></> : selectedTransit ? <> · In transit <b>{minutesToRunTime(selectedTransit.running_transit_minutes)}</b></> : null}</p>
            <div>
              <span>{selected.department_name||"Department unmapped"}</span>
              <span>{selectedSiteCode||selected.site_name||selected.site_code||"Site unmapped"}</span>
              <span>{selected.designation_name||"Designation unmapped"}</span>
            </div>
            <p style={{marginTop:8}}>{selectedState.message}</p>
          </div>
          <mark className={selectedState.state==="INSIDE"?"inside":""}>{selectedState.label}</mark>
        </div>}

        {selected && <div className="cp-punch-pad">
          <button disabled={actionBusy || !selectedState.allowed.in} className="primary" onClick={()=>punch(selected,"in","NORMAL_IN","IN","in")}>IN</button>
          <button disabled={actionBusy || !selectedState.allowed.out} onClick={()=>punch(selected,"out","FINAL_OUT","OUT","out")}>OUT</button>
          <button disabled={actionBusy || !selectedState.allowed.break} onClick={()=>punch(selected,"out","BREAK_OUT","On Break","break")}>On Break</button>
          <button disabled={actionBusy || !selectedState.allowed.return} onClick={()=>punch(selected,"in","RETURN_FROM_BREAK","Return from Break","return")}>Return from Break</button>
        </div>}
        {selected && <div className="cp-punch-pad" style={{marginTop:8}}>
          <button disabled={actionBusy || !selectedState.allowed.transfer} onClick={startTransfer}>Send to Site</button>
          <button disabled={actionBusy || !selectedState.allowed.receive} className="primary" onClick={()=>receiveTransit()}>Receive In</button>
          <button disabled={actionBusy || !selectedState.allowed.officialOut} onClick={()=>gateAction("OFFICIAL_DUTY_OUT",{label:"Official Duty Out"})}>Official Duty Out</button>
          <button disabled={actionBusy || !selectedState.allowed.officialReturn} onClick={()=>gateAction("OFFICIAL_DUTY_RETURN",{label:"Official Duty Return"})}>Official Duty Return</button>
          <button disabled={actionBusy || !selectedState.allowed.breakToOut} onClick={()=>gateAction("BREAK_TO_OUT",{label:"Break → OUT"})}>Break → OUT</button>
          <button disabled={actionBusy || !selectedState.allowed.transitOut} onClick={()=>gateAction("TRANSIT_OUT",{label:"Transit → OUT"})}>Transit → OUT</button>
          <button onClick={submitManualOverride}>Manual Override</button>
          <button onClick={reload}>Refresh</button>
        </div>}

        <div className="cp-suggestion-header">Employee Suggestions</div>
        <div className="cp-suggestion-list">
          {loading?<div className="cp-empty">Loading employees…</div>:
          !hasSearch?<div className="cp-empty">Type E-code or name to see suggestions.</div>:
          suggestions.length?suggestions.map((e,i)=>
            <button key={e.id} className={cn("cp-suggestion",selected?.id===e.id&&"selected",i===activeSuggestion&&"active")} onClick={()=>choose(e)}>
              <Avatar name={e.full_name} path={e.photo_path}/>
              <div className="cp-suggestion-main">
                <div><b>{e.full_name}</b></div>
                <span><strong className="cp-ecode-inline">E-code: {e.employee_code}</strong> · {personMeta(e)}</span>
              </div>
              <small className={cn(e._state==="INSIDE"&&"inside")}>{e._stateLabel}</small>
            </button>
          ):<div className="cp-empty">No matching employee found.</div>}
        </div>


      </section>

      <section className="cp-card cp-visitor-card">
        <div className="cp-card-title">
          <div><b>Visitors / Vendors</b><span>Currently inside and person to meet</span></div>
          <em>{visitors.length}</em>
        </div>
        <div className="cp-visitor-list">
          {visitors.length?visitors.slice(0,7).map(v=>
            <div className="cp-visitor-row" key={v.id}>
              <div className="avatar">{init(v.visitor_name)}</div>
              <div>
                <b>{v.visitor_name}</b>
                <span>{v.organization||"—"} · To meet: {v.host_employee_name||v.host_name||"—"}</span>
                <em>{v.site_name||"—"} · Entry {timeOnly(v.entry_time)} · Inside {runTime(v.entry_time)}</em>
              </div>
              <small className={cn(v.overstay_flag&&"warn")}>{v.overstay_flag?"Overstay":runTime(v.entry_time)}</small>
            </div>
          ):<div className="cp-empty">No active visitors.</div>}
        </div>
      </section>

      <aside className="cp-card cp-context-card cp-stacked-context">
        <div className="cp-stack-section">
          <div className="cp-stack-title"><b>Employees Inside</b><em>{inside.length}</em></div>
          <div className="cp-context-list cp-stack-scroll">
            {inside.slice(0,8).map(x=>
              <div className="cp-mini-row" key={x.employee_id}>
                <Avatar name={x.full_name} path={x.photo_path}/>
                <div><b>{x.employee_code} — {x.full_name}</b><span>{x.department_name||"—"} · {x.site_code||"—"} · Inside {runTime(x.last_event_time)}</span></div>
                <small>{x.site_code||"—"}</small>
              </div>
            )}
            {!inside.length && <div className="cp-empty">No employees inside.</div>}
          </div>
        </div>

        <div className="cp-stack-section">
          <div className="cp-stack-title"><b>Visitors Inside</b><em>{visitors.length}</em></div>
          <div className="cp-context-list cp-stack-scroll small">
            {visitors.slice(0,6).map(v=>
              <div className="cp-mini-row" key={v.id}>
                <div className="avatar">{init(v.visitor_name)}</div>
                <div><b>{v.visitor_name}</b><span>{v.organization||"—"} · To meet: {v.host_employee_name||v.host_name||"—"} · {runTime(v.entry_time)}</span></div>
                <small>{v.overstay_flag?"overstay":"inside"}</small>
              </div>
            )}
            {!visitors.length && <div className="cp-empty">No active visitors.</div>}
          </div>
        </div>

        <div className="cp-stack-section">
          <div className="cp-stack-title"><b>In Transit</b><em>{transits.length}</em></div>
          <div className="cp-context-list cp-stack-scroll small">
            {transits.map(t=>
              <div className="cp-mini-row" key={t.id}>
                <Avatar name={t.full_name} path={t.employee?.photo_path}/>
                <div><b>{t.employee_code} — {t.full_name}</b><span>{t.from_site_code||"—"} → {t.to_site_code||"—"} · {minutesToRunTime(t.running_transit_minutes)}</span></div>
                <button className="btn teal small" onClick={()=>receiveTransit(t)}>Receive</button>
              </div>
            )}
            {!transits.length && <div className="cp-empty">No employee in transit.</div>}
          </div>
        </div>

        <div className="cp-stack-section">
          <div className="cp-stack-title"><b>Recent / Alerts</b><em>{alerts.length}</em></div>
          <div className="cp-context-list cp-stack-scroll small">
            {alerts.length?alerts.slice(0,4).map((a,i)=>
              <div className="cp-alert-row" key={i}><small className={a.tone}>{a.kind}</small><b>{a.title}</b><span>{a.body}</span></div>
            ):recent.slice(0,5).map(ev=>
              <div className="cp-mini-row" key={ev.id}>
                <Avatar name={ev.employee?.full_name} path={ev.employee?.photo_path}/>
                <div><b>{ev.employee?.employee_code||ev.emp?.employee_code} — {ev.employee?.full_name||ev.emp?.full_name||"Employee"}</b><span>{String(ev.event_type||ev.type)} · {timeOnly(ev.event_time||ev.time)}</span></div>
                <small>{String(ev.event_type||ev.type)}</small>
              </div>
            )}
          </div>
        </div>
      </aside>
    </div>
  </div>
}
function GuardInside(){
  const [tick,setTick]=useState(Date.now());
  useEffect(()=>{
    const id=setInterval(()=>setTick(Date.now()),60000);
    return ()=>clearInterval(id);
  },[]);
  const {data,loading,error,reload}=useLoad(async()=>{
    const [inside,visitors]=await Promise.all([
      supabase.from("v_people_inside_now").select("*").order("site_code"),
      supabase.from("v_active_visitors").select("*").order("entry_time",{ascending:false})
    ]);
    if(inside.error)throw inside.error;
    if(visitors.error)throw visitors.error;
    const ids=[...new Set((inside.data||[]).map(x=>x.employee_id).filter(Boolean))];
    let empMap={};
    if(ids.length){
      const {data:emps,error:empErr}=await supabase.from("employees").select("id,photo_path,designation_id").in("id",ids);
      if(empErr)throw empErr;
      empMap=Object.fromEntries((emps||[]).map(e=>[e.id,e]));
    }
    return {
      inside:(inside.data||[]).map(x=>({...x, photo_path:empMap[x.employee_id]?.photo_path})),
      visitors:visitors.data||[]
    };
  },[]);
  const [site,setSite]=useState("all");
  const [view,setView]=useState("grid");
  if(loading)return <div className="empty">Loading who's inside...</div>;
  if(error)return <div className="error">{error}</div>;
  const filtered=data.inside.filter(x=>site==="all"||x.site_code===site);

  return <div>
    <div className="toolbar">
      <div className="segment">
        <button className={site==="all"?"active":""} onClick={()=>setSite("all")}>All</button>
        <button className={site==="P77"?"active":""} onClick={()=>setSite("P77")}>Plot 77</button>
        <button className={site==="P79"?"active":""} onClick={()=>setSite("P79")}>Plot 79</button>
      </div>
      <div className="segment">
        <button className={view==="grid"?"active":""} onClick={()=>setView("grid")}>Grid</button>
        <button className={view==="list"?"active":""} onClick={()=>setView("list")}>List</button>
      </div>
      <button className="btn ghost" onClick={reload}>Refresh</button>
    </div>

    <div className="grid cols3">
      <Stat label="Employees Inside" value={filtered.length} icon="👥" tone="green"/>
      <Stat label="Visitors Inside" value={data.visitors.length} icon="🏷" tone="amber"/>
      <Stat label="Total On-Site" value={filtered.length+data.visitors.length} icon="◎"/>
    </div>

    <div className="grid cols2" style={{marginTop:18}}>
      <div className="card premium">
        <div className="cardTitle"><h3>Employees Inside</h3><span className="badge green">{filtered.length}</span></div>
        {view==="grid" ? <div className="insideGrid">
          {filtered.map(x=><div className="insideTile" key={x.employee_id}>
            <Avatar name={x.full_name} path={x.photo_path}/>
            <b>{x.employee_code} — {x.full_name}</b>
            <span>{x.department_name||"—"}</span>
            <em>{x.site_name||"—"} · Since {timeOnly(x.last_event_time)} · Inside {runTime(x.last_event_time)}</em>
            <small className="badge green">Inside</small>
          </div>)}
        </div> : <div>
          {filtered.map(x=><div className="person-row" key={x.employee_id}>
            <Avatar name={x.full_name} path={x.photo_path}/>
            <div><div className="name">{x.employee_code} — {x.full_name}</div><div className="meta">{x.department_name||"—"} · {x.site_name||"—"} · Since {timeOnly(x.last_event_time)} · Inside {runTime(x.last_event_time)}</div></div>
            <span className="badge green">Inside</span>
          </div>)}
        </div>}
      </div>

      <div className="card premium">
        <div className="cardTitle"><h3>Visitors / Vendors Inside</h3><span className="badge amber">{data.visitors.length}</span></div>
        {view==="grid" ? <div className="insideGrid">
          {data.visitors.map(v=><div className="insideTile visitor" key={v.id}>
            <div className="avatar">{init(v.visitor_name)}</div>
            <b>{v.visitor_name}</b>
            <span>{v.organization||"—"}</span>
            <em>{v.site_name||"—"} · Host: {v.host_employee_name||v.host_name||"—"}</em>
            <small className={cn("badge",v.overstay_flag?"amber":"green")}>{v.overstay_flag?`Overstay ${runTime(v.entry_time)}`:`${runTime(v.entry_time)}`}</small>
          </div>)}
        </div> : <div>
          {data.visitors.map(v=><div className="person-row" key={v.id}>
            <div className="avatar">{init(v.visitor_name)}</div>
            <div><div className="name">{v.visitor_name}</div><div className="meta">{v.organization||"—"} · {v.site_name||"—"} · Host: {v.host_employee_name||v.host_name||"—"}</div></div>
            <span className={cn("badge",v.overstay_flag?"amber":"green")}>{v.overstay_flag?"Overstay":"Inside"}</span>
          </div>)}
        </div>}
      </div>
    </div>
  </div>
}
function GuardVisitors({profile}){
  const {data,loading,reload}=useLoad(async()=>{const [active,freq,exit]=await Promise.all([supabase.from("v_active_visitors").select("*"),supabase.from("frequent_visitors").select("*").eq("status","active").order("visitor_name"),supabase.from("exit_reasons").select("*").in("applies_to",["visitor_out","all"]).order("display_order")]);return {active:active.data||[],freq:freq.data||[],exit:exit.data||[]}},[]);
  const [v,setV]=useState({visitor_name:"",phone:"",organization:"",host_name:"",purpose:"Meeting",vehicle_number:""});
  const [visErr,setVisErr]=useState("");
  async function log(){if(!v.visitor_name||!v.phone||!v.host_name){setVisErr("Name, phone, and person to meet are required.");return}const {error}=await supabase.from("visitor_sessions").insert({tenant_id:profile.tenant_id,...v,status:"inside",entry_guard_user_id:profile.id,entry_time:new Date().toISOString(),created_by:profile.id,updated_by:profile.id});if(error){setVisErr(error.message);}else{setVisErr("");setV({visitor_name:"",phone:"",organization:"",host_name:"",purpose:"Meeting",vehicle_number:""});reload();}}
  async function exitVisitor(row){const reason=data.exit[0]?.id||null;const {error}=await supabase.from("visitor_sessions").update({status:"exited",exit_time:new Date().toISOString(),exit_guard_user_id:profile.id,exit_reason_id:reason,updated_by:profile.id}).eq("id",row.id);if(error)console.error(error.message);else reload();}
  if(loading)return <div className="empty">Loading visitor module...</div>;
  return <div className="grid cols2">
    <div className="card premium"><div className="cardTitle"><h3>Visitor / Vendor Entry</h3><span className="badge goldb">Gate Register</span></div><div className="formGrid" style={{gridTemplateColumns:"1fr 1fr"}}>{["visitor_name","phone","organization","host_name","vehicle_number"].map(k=><div key={k}><label className="label">{k.replaceAll("_"," ")}</label><input className="input" value={v[k]} onChange={e=>setV({...v,[k]:e.target.value})}/></div>)}<div><label className="label">Purpose</label><select className="select" value={v.purpose} onChange={e=>setV({...v,purpose:e.target.value})}><option>Meeting</option><option>Delivery</option><option>Audit</option><option>Interview</option><option>Vendor Visit</option><option>Maintenance</option><option>Other</option></select></div></div>{visErr&&<div className="error" style={{marginTop:8,padding:"6px 10px",background:"var(--red-t)",borderRadius:8}}>{visErr}</div>}
<button className="btn gold" style={{marginTop:14,width:"100%"}} onClick={()=>{setVisErr("");log();}}>Log Visitor Entry</button><h3 style={{marginTop:22}}>Frequent Visitors</h3>{data.freq.map(f=><div className="glass-note" key={f.id} style={{marginBottom:8}}><b>{f.visitor_name}</b> · {f.organization||"—"} · {f.phone||"—"}</div>)}</div>
    <div className="card premium"><div className="cardTitle"><h3>Currently Inside</h3><span className="badge amber">{data.active.length}</span></div>{data.active.map(a=><div className="person-row" key={a.id}><div className="avatar">{init(a.visitor_name)}</div><div><div className="name">{a.visitor_name}</div><div className="meta">{a.organization||"—"} · {a.purpose||"—"} · Entry: {timeOnly(a.entry_time)} · Inside: {runTime(a.entry_time)}</div></div><button className="btn ghost small" onClick={()=>exitVisitor(a)}>Exit</button></div>)}</div>
  </div>
}
function GuardLookup(){return <Reports/>}
function GuardManual({profile}){return <Approvals profile={profile}/>}
function GuardEmergency(){return <EmergencyRoster/>}
function GuardHandover({profile}){
  const {data,loading,reload}=useLoad(async()=>{const [inside,visitors,logs]=await Promise.all([supabase.from("v_people_inside_now").select("*"),supabase.from("v_active_visitors").select("*"),supabase.from("guard_handover_logs").select("*").order("handover_time",{ascending:false}).limit(10)]);return {inside:inside.data||[],visitors:visitors.data||[],logs:logs.data||[]}},[]);
  const [out,setOut]=useState(localStorage.getItem("guardName")||""),[inc,setInc]=useState("");
  const [handoverErr,setHandoverErr]=useState("");
  async function save(){const {error}=await supabase.from("guard_handover_logs").insert({tenant_id:profile.tenant_id,outgoing_guard_user_id:profile.id,outgoing_guard_name:out,incoming_guard_name:inc,people_inside_count:data.inside.length,active_visitors_count:data.visitors.length,pending_sync_count:0,notes:"Handover recorded from guard portal",created_by:profile.id});if(error)setHandoverErr(error.message);else{setHandoverErr("");reload();}}
  if(loading)return <div className="empty">Loading handover...</div>;
  return <div className="grid cols2"><div className="card premium"><h3>Record Handover</h3><label className="label">Outgoing guard</label><input className="input" value={out} onChange={e=>setOut(e.target.value)}/><label className="label">Incoming guard</label><input className="input" value={inc} onChange={e=>setInc(e.target.value)}/><div className="glass-note" style={{marginTop:14}}>People inside: <b>{data.inside.length}</b><br/>Visitors inside: <b>{data.visitors.length}</b><br/>Pending sync: <b>0</b></div>{handoverErr&&<div className="error" style={{marginTop:8}}>{handoverErr}</div>}
<button className="btn gold" style={{marginTop:14}} onClick={save}>Save Handover</button></div><div className="card premium"><h3>Recent Handovers</h3>{data.logs.map(l=><div className="glass-note" key={l.id} style={{marginBottom:8}}><b>{l.outgoing_guard_name||"—"} → {l.incoming_guard_name||"—"}</b><br/>{fmt(l.handover_time)} · People: {l.people_inside_count} · Visitors: {l.active_visitors_count}</div>)}</div></div>
}

/* Shared people/user screens */

function Employees(){
  const [q,setQ]=useState(""); const [editing,setEditing]=useState(null); const [photoFile,setPhotoFile]=useState(null); const [saving,setSaving]=useState(false); const [empError,setEmpError]=useState(""); const [statusModal,setStatusModal]=useState(null);
  const blank={employee_code:"",full_name:"",status:"active",date_of_joining:today(),last_working_day:"",site_code:"",division_code:"",department_code:"",sub_department_code:"",team_cell_code:"",org_chart_node_code:"",designation_name:"",designation_track:"",band:"",grade:"",employee_type_code:"",reporting_manager_code:"",functional_head_code:"",hr_partner_code:"",official_email:"",personal_email:"",personal_mobile:"",photo_path:"",gate_access_enabled:true,self_punch_enabled:true,is_first_aider:false,is_fire_marshal:false,is_field_marshal:false};
  const [form,setForm]=useState(blank);
  const {data,loading,error,reload}=useLoad(async()=>{
    const [empRes,masters,contact,access,medical]=await Promise.all([
      supabase.from("employees").select("*").order("employee_code").limit(5000), loadSetupMasters(),
      supabase.from("employee_contact_addresses").select("employee_id").limit(10000),
      supabase.from("employee_access_attendance_profiles").select("employee_id").limit(10000),
      supabase.from("employee_medical_emergency").select("employee_id").limit(10000)
    ]);
    if(empRes.error)throw empRes.error;
    const maps=await getOrgMaps();
    const contacts=new Set((contact.data||[]).map(x=>x.employee_id)), accesses=new Set((access.data||[]).map(x=>x.employee_id)), medicals=new Set((medical.data||[]).map(x=>x.employee_id));
    return {employees:(empRes.data||[]).map(e=>({...withOrg(e,maps),has_contact:contacts.has(e.id),has_access:accesses.has(e.id),has_medical:medicals.has(e.id)})),masters};
  },[]);
  if(loading)return <div className="empty">Loading employees...</div>; if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  const employees=data.employees||[], masters=data.masters||{};
  const rows=employees.filter(e=>`${e.employee_code} ${e.full_name} ${e.department_name||""} ${e.site_code||""} ${e.status||""} ${e.division_code||""} ${e.sub_department_code||""} ${e.team_cell_code||""} ${e.band||""} ${e.grade||""}`.toLowerCase().includes(q.toLowerCase()));
  const orgs=masters.orgs||[];
  const rootNodes=orgs.filter(o=>!o.parent_org_unit_code || o.parent_org_unit_code==="MD" || ["directorate","function","department"].includes(o.org_unit_type) && !o.parent_org_unit_code);
  const childrenOfAny=(code)=>code ? orgs.filter(o=>o.parent_org_unit_code===code) : [];
  const divisions=orgs.filter(o=>o.parent_org_unit_code==="MD" || (!o.parent_org_unit_code && o.org_unit_code!=="MD"));
  const departments=form.division_code ? childrenOfAny(form.division_code) : [];
  const subDepartments=form.department_code ? childrenOfAny(form.department_code) : [];
  const teams=form.sub_department_code ? childrenOfAny(form.sub_department_code) : [];
  const gradeMatrix=masters.gradeMatrix||[];
  const roleFitmentRows=masters.roleFitments||[];
  const roleFitmentOptions=roleFitmentRows.map(r=>({
    ...r,
    code:r.role_fitment_code,
    label:`${typeBadgeLabel(r.track)} · ${r.band||"—"} · ${r.grade||"—"} · ${r.designation_name||"—"}`,
    pathLabel:`${r.education_match||"No education match"} · ${r.indicative_lower_years??"?"}-${r.indicative_upper_years??"?"} yrs`
  }));
  const tracks=[...new Set(gradeMatrix.map(g=>g.track).filter(Boolean))].map(x=>({code:x,label:x.replace(/_/g," ")}));
  const bands=[...new Set(gradeMatrix.filter(g=>!form.designation_track||g.track===form.designation_track).map(g=>g.band).filter(Boolean))].map(x=>({code:x,label:x}));
  const grades=[...new Set(gradeMatrix.filter(g=>(!form.designation_track||g.track===form.designation_track)&&(!form.band||g.band===form.band)).map(g=>g.grade).filter(Boolean))].map(x=>({code:x,label:x}));
  const designations=[...new Set(gradeMatrix.filter(g=>(!form.designation_track||g.track===form.designation_track)&&(!form.band||g.band===form.band)&&(!form.grade||g.grade===form.grade)).flatMap(g=>[g.business_designation,g.science_designation,g.engineering_designation]).filter(Boolean))].map(x=>({code:x,label:x}));
  function applyRoleFitment(code){
    const r=roleFitmentOptions.find(x=>x.code===code);
    if(!r){setForm({...form,role_fitment_code:""});return}
    setForm({...form,
      role_fitment_code:code,
      designation_track:r.track||"",
      band:r.band||"",
      grade:r.grade||"",
      designation_name:r.designation_name||"",
      education_match:r.education_match||"",
      role_fitment_matrix_id:r.matrix_id||""
    });
  }
  function edit(e){setEditing(e);setPhotoFile(null);setForm({...blank,...e,site_code:e.site_code||"",department_code:e.department_code||"",official_email:e.official_email||"",personal_email:e.personal_email||"",personal_mobile:e.personal_mobile||e.mobile||""});}
  function addNew(){setEditing({new:true});setPhotoFile(null);setForm(blank);}
  function setField(k,v){const n={...form,[k]:v}; if(k==="division_code"){n.department_code="";n.sub_department_code="";n.team_cell_code="";n.org_chart_node_code="";} if(k==="department_code"){n.sub_department_code="";n.team_cell_code="";n.org_chart_node_code="";} if(k==="sub_department_code"){n.team_cell_code="";n.org_chart_node_code="";} if(k==="designation_track"){n.band="";n.grade="";n.designation_name="";} if(k==="band"){n.grade="";n.designation_name="";} if(k==="grade"){n.designation_name="";} setForm(n);}
  async function save(){if(!form.employee_code||!form.full_name){setEmpError("Employee Code and Full Name are required.");return} if(!form.site_code){setEmpError("Site is required for guard movement and attendance logic.");return} setSaving(true); try{let payload={...form}; if(photoFile)payload.photo_path=await uploadEmployeePhoto(photoFile,form.employee_code); const {error}=await supabase.rpc("rpc_hr_upsert_employee",{p_employee:payload}); if(error)throw error; setEditing(null);setForm(blank);setPhotoFile(null);reload();}catch(e){setEmpError(e.message||String(e));}finally{setSaving(false);}}
  async function setStatus(e,status,reason,lwd=null){const {error}=await supabase.rpc("rpc_hr_set_employee_status",{p_employee_id:e.id,p_status:status,p_last_working_day:lwd||null,p_gate_access_enabled:status==="active",p_reason:reason||"HR status update"}); if(error)return error.message; reload(); return null;}
  function exportRows(){downloadCsv("employee_master_current_filtered.csv",rows.map(e=>({employee_code:e.employee_code,full_name:e.full_name,status:e.status,date_of_joining:e.date_of_joining,site_code:e.site_code,division_code:e.division_code,department_code:e.department_code,sub_department_code:e.sub_department_code,team_cell_code:e.team_cell_code,designation_name:e.designation_name,designation_track:e.designation_track,band:e.band,grade:e.grade,employee_type_code:e.employee_type_code,reporting_manager_code:e.reporting_manager_code,functional_head_code:e.functional_head_code,hr_partner_code:e.hr_partner_code})));}
  return <div className="grid emp-page">
    <div className="toolbar emp-toolbar"><input className="input" style={{maxWidth:420}} placeholder="Search employee, department, site, band, grade, gap..." value={q} onChange={e=>setQ(e.target.value)}/><button className="btn gold" onClick={addNew}>Add Employee</button><button className="btn ghost" onClick={exportRows}>Download Current View</button><button className="btn ghost" onClick={reload}>Refresh</button></div>
    <div className="card premium emp-gap-card"><div className="cardTitle"><h3>Employee Master V4 Readiness</h3><span className="badge goldb">{employees.length} employees</span></div><div className="emp-gap-row"><span>Missing Site: <b>{employees.filter(e=>!e.site_code).length}</b></span><span>Missing Department: <b>{employees.filter(e=>!e.department_code&&!e.department_name).length}</b></span><span>Missing Grade: <b>{employees.filter(e=>!e.grade).length}</b></span><span>Missing Manager: <b>{employees.filter(e=>!e.reporting_manager_code&&!e.reporting_manager_id).length}</b></span><span>Contact Rows: <b>{employees.filter(e=>e.has_contact).length}</b></span><span>Access Rows: <b>{employees.filter(e=>e.has_access).length}</b></span><span>Emergency Rows: <b>{employees.filter(e=>e.has_medical).length}</b></span></div></div>
    {editing&&<div className="card premium emp-form-card"><div className="cardTitle"><h3>{editing.new?"Add Employee":"Edit Employee"}</h3><button className="btn ghost small" onClick={()=>setEditing(null)}>Close</button></div>
      <div className="section-title">Core</div><div className="formGrid emp-form-grid"><TextInput field="employee_code" value={form.employee_code} onChange={v=>setField("employee_code",v)} required/><TextInput field="full_name" value={form.full_name} onChange={v=>setField("full_name",v)} required/><SmartSelect field="status" label="Employee Status" value={form.status} onChange={v=>setField("status",v)} options={["draft","active","inactive","on_notice","exited","suspended"].map(x=>({code:x,label:statusLabel(x)}))}/><TextInput field="date_of_joining" type="date" value={form.date_of_joining} onChange={v=>setField("date_of_joining",v)} required/><TextInput field="last_working_day" type="date" value={form.last_working_day} onChange={v=>setField("last_working_day",v)}/><SmartSelect field="site_code" label="Site" value={form.site_code} onChange={v=>setField("site_code",v)} options={(masters.sites||[]).map(s=>({...s,code:s.site_code,label:s.site_name}))} required/><SmartSelect field="employee_type_code" label="Employee Type" value={form.employee_type_code} onChange={v=>setField("employee_type_code",v)} options={(masters.employeeTypes||[]).map(t=>({...t,code:t.employee_type_code,label:t.employee_type_name}))}/></div>
      <div className="section-title">Organization</div><div className="field-help org-form-help">Employee org path uses direct children from Setup Data. Select parent → child → child. No rigid department assumptions.</div><div className="formGrid emp-form-grid"><SmartSelect field="division_code" label="Reporting Line / Directorate" value={form.division_code} onChange={v=>setField("division_code",v)} options={divisions.map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))}/><SmartSelect field="department_code" label="Function / Department" value={form.department_code} onChange={v=>setField("department_code",v)} options={departments.map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))}/><SmartSelect field="sub_department_code" label="Sub-Department / Workstream" value={form.sub_department_code} onChange={v=>setField("sub_department_code",v)} options={subDepartments.map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))} disabled={!form.department_code}/><SmartSelect field="team_cell_code" label="Team / Cell" value={form.team_cell_code} onChange={v=>setField("team_cell_code",v)} options={teams.map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))} disabled={!form.sub_department_code}/><SmartSelect field="org_chart_node_code" label="Org Chart Node" value={form.org_chart_node_code} onChange={v=>setField("org_chart_node_code",v)} options={[...teams,...subDepartments,...departments,...divisions].map(o=>({...o,code:o.org_unit_code,label:o.org_unit_name,pathLabel:orgPathLabel(o,orgs)}))}/></div>
      <div className="section-title">Role / Grade</div>
      <div className="field-help org-form-help">Select one valid role-fitment row. Track, Band, Grade and Designation are filled from the matrix so HR does not manually combine mismatched values.</div>
      <div className="formGrid emp-form-grid">
        <SmartSelect field="role_fitment_code" label="Role / Grade Fitment" value={form.role_fitment_code} onChange={applyRoleFitment} options={roleFitmentOptions}/>
        <div><FieldLabel field="designation_track"/><input className="input" disabled value={form.designation_track||""}/></div>
        <div><FieldLabel field="band"/><input className="input" disabled value={form.band||""}/></div>
        <div><FieldLabel field="grade"/><input className="input" disabled value={form.grade||""}/></div>
        <div><FieldLabel field="designation_name"/><input className="input" disabled value={form.designation_name||""}/></div>
        <div><FieldLabel field="education_match"/><input className="input" disabled value={form.education_match||""}/></div>
      </div>
      <div className="section-title">Reporting</div><div className="formGrid emp-form-grid"><SmartSelect field="reporting_manager_code" label="Reporting Manager" value={form.reporting_manager_code} onChange={v=>setField("reporting_manager_code",v)} options={employees.map(e=>({...e,code:e.employee_code,label:e.full_name}))}/><SmartSelect field="functional_head_code" label="Functional Head" value={form.functional_head_code} onChange={v=>setField("functional_head_code",v)} options={employees.map(e=>({...e,code:e.employee_code,label:e.full_name}))}/><SmartSelect field="hr_partner_code" label="HR Partner" value={form.hr_partner_code} onChange={v=>setField("hr_partner_code",v)} options={employees.map(e=>({...e,code:e.employee_code,label:e.full_name}))}/></div>
      <div className="section-title">Contact / Access / Safety</div><div className="formGrid emp-form-grid"><TextInput field="official_email" value={form.official_email} onChange={v=>setField("official_email",v)}/><TextInput field="personal_email" value={form.personal_email} onChange={v=>setField("personal_email",v)}/><TextInput field="personal_mobile" value={form.personal_mobile} onChange={v=>setField("personal_mobile",v)}/></div><div className="emp-photo-box"><Avatar name={form.full_name} path={form.photo_path}/><div><FieldLabel field="photo_path">Employee Photo</FieldLabel><input className="input" type="file" accept="image/*" onChange={e=>setPhotoFile(e.target.files?.[0]||null)}/><div className="field-help">{photoFile?`Selected: ${photoFile.name}`:"Use file upload. Do not type photo paths."}</div></div></div><div className="formGrid emp-check-grid">{[["gate_access_enabled","Gate Access"],["self_punch_enabled","Self Punch"],["is_first_aider","First Aider"],["is_fire_marshal","Fire Marshal"],["is_field_marshal","Field Marshal"]].map(([k,l])=><label key={k} className="checkLine"><input type="checkbox" checked={!!form[k]} onChange={e=>setField(k,e.target.checked)}/>{l}</label>)}</div>{empError&&<div className="error" style={{marginTop:8,padding:"8px 12px",background:"var(--red-t)",borderRadius:10}}>{empError}</div>}
      <div className="toolbar"><button className="btn gold" disabled={saving} onClick={()=>{setEmpError("");save();}}>Save Employee</button><button className="btn ghost" onClick={()=>{setEditing(null);setEmpError("");}}>Cancel</button></div></div>}
    <div className="table emp-master-grid"><table><thead><tr>{["Photo","Employee Code","Name","Status","Site","Department","Sub-Department","Team / Cell","Designation","Band","Grade","Manager","Modules","Actions"].map(h=><th key={h}>{h}</th>)}</tr></thead><tbody>{rows.map(e=><tr key={e.id}><td><Avatar name={e.full_name} path={e.photo_path}/></td><td>{e.employee_code}</td><td><b>{e.full_name}</b></td><td><span className={cn("badge",e.status==="active"?"green":"amber")}>{statusLabel(e.status)}</span></td><td>{e.site_code||"—"}</td><td>{e.department_name||e.department_code||"—"}</td><td>{e.sub_department_code||"—"}</td><td>{e.team_cell_code||"—"}</td><td>{e.designation_name||"—"}</td><td>{e.band||"—"}</td><td>{e.grade||"—"}</td><td>{e.reporting_manager_code||"—"}</td><td><span className={cn("badge",moduleStatusClass(e.has_contact))}>Contact</span> <span className={cn("badge",moduleStatusClass(e.has_access))}>Access</span> <span className={cn("badge",moduleStatusClass(e.has_medical))}>Emergency</span></td><td><div className="toolbar" style={{margin:0}}><button className="btn ghost small" onClick={()=>edit(e)}>Edit</button><button className="btn ghost small" onClick={()=>setStatus(e,"inactive")}>Pause</button><button className="btn ghost small" onClick={()=>setStatusModal({emp:e,status:"exited"})}>Exit</button></div></td></tr>)}</tbody></table></div>
  </div>
}


function BulkEmployeeUpload({profile}){
  const REQUIRED_SHEET="Employee Master Upload";
  const [rows,setRows]=useState([]);
  const [batch,setBatch]=useState(null);
  const [result,setResult]=useState(null);
  const [busy,setBusy]=useState(false);
  const [fileName,setFileName]=useState("");
  const [warnings,setWarnings]=useState([]);
  const [previewMode,setPreviewMode]=useState("all");

  const professionalEmployeeHeaders = [
    // ── CORE IDENTITY ─────────────────────────────────────────────────────
    {group:"Core Identity", header:"Employee Code", field:"employee_code", required:"Yes", help:"Unique employee code / E-code. Key for update/create. Example: 520."},
    {group:"Core Identity", header:"Employee Full Name", field:"full_name", required:"Yes", help:"Full name as it should appear in all HR records."},
    {group:"Core Identity", header:"Employment Status", field:"status", required:"Yes", help:"active / paused / exited / on_notice / draft."},
    {group:"Core Identity", header:"Date of Joining", field:"date_of_joining", required:"Yes", help:"Recommended DD-MM-YYYY. Common Excel date formats are accepted."},
    {group:"Core Identity", header:"Last Working Day", field:"last_working_day", required:"Conditional", help:"Required when status is exited. Recommended DD-MM-YYYY. Leave blank otherwise."},
    {group:"Core Identity", header:"Employee Type", field:"employee_type_code", required:"Recommended", help:"ONROLL, CONTRACT, TRAINEE, INTERN, CONSULTANT, VENDOR."},
    // ── PERSONAL PROFILE ──────────────────────────────────────────────────
    {group:"Personal Profile", header:"Date of Birth", field:"date_of_birth", required:"Optional", help:"Recommended DD-MM-YYYY. Common Excel date formats are accepted. Used for birthday events and age calculations."},
    {group:"Personal Profile", header:"Gender", field:"gender", required:"Optional", help:"Male / Female / Non-Binary / Prefer Not to Say."},
    {group:"Personal Profile", header:"Blood Group", field:"blood_group", required:"Optional", help:"A+, B+, AB+, O+, A-, B-, AB-, O-. Used for emergency and muster purposes."},
    {group:"Personal Profile", header:"Marital Status", field:"marital_status", required:"Optional", help:"Single / Married / Divorced / Widowed."},
    {group:"Personal Profile", header:"Nationality", field:"nationality", required:"Optional", help:"Indian / Other. Used for statutory compliance."},
    // ── CONTACT ───────────────────────────────────────────────────────────
    {group:"Contact Details", header:"Official Email", field:"official_email", required:"Recommended", help:"Official email used for portal login reference."},
    {group:"Contact Details", header:"Personal Email", field:"personal_email", required:"Optional", help:"Personal / alternate email."},
    {group:"Contact Details", header:"Mobile Number", field:"personal_mobile", required:"Recommended", help:"Primary mobile number."},
    {group:"Contact Details", header:"Alternate Mobile", field:"alternate_mobile", required:"Optional", help:"Secondary contact mobile."},
    // ── WORK LOCATION ─────────────────────────────────────────────────────
    {group:"Work Location", header:"Primary Site", field:"site_code", required:"Recommended", help:"P77 / P79 or site code from Setup. NA means unassigned."},
    {group:"Work Location", header:"Gate Access Required", field:"gate_access_enabled", required:"Optional", help:"TRUE / FALSE. Whether employee appears in gate/punch flows."},
    {group:"Work Location", header:"Self Punch Access", field:"self_punch_enabled", required:"Optional", help:"TRUE / FALSE. Whether employee can self-punch via portal."},
    // ── ORG ASSIGNMENT ────────────────────────────────────────────────────
    {group:"Organization Assignment", header:"Function", field:"division_code", required:"Optional", help:"Reporting line / directorate. E.g. Director Operations, Quality."},
    {group:"Organization Assignment", header:"Department", field:"department_code", required:"Recommended", help:"Department or function code from Setup Data."},
    {group:"Organization Assignment", header:"Sub-Department", field:"sub_department_code", required:"Optional", help:"Sub-department, section, lab or process if applicable."},
    {group:"Organization Assignment", header:"Team / Cell", field:"team_cell_code", required:"Optional", help:"Team, cell, line, lab or specialization if applicable."},
    {group:"Organization Assignment", header:"Primary Organization Unit", field:"org_chart_node_code", required:"Optional", help:"Org unit / org chart node code from Org Builder. Leave blank if not yet mapped."},
    // ── ROLE / GRADE ──────────────────────────────────────────────────────
    {group:"Role / Grade", header:"Designation", field:"designation_name", required:"Recommended", help:"Designation title. E.g. Vice President, Senior Scientist."},
    {group:"Role / Grade", header:"Designation Track", field:"designation_track", required:"Optional", help:"Business Enablement / Science Led / Engineering / Technician / Lab Support."},
    {group:"Role / Grade", header:"Band", field:"band", required:"Optional", help:"Band / tier from grade matrix. Example: Band 2, Band 7."},
    {group:"Role / Grade", header:"Grade", field:"grade", required:"Recommended", help:"Grade code from matrix. Example: 2-I, 7-II, 8-I."},
    // ── REPORTING RELATIONSHIPS ───────────────────────────────────────────
    {group:"Reporting Relationships", header:"Reporting Manager Employee Code", field:"reporting_manager_code", required:"Recommended", help:"Employee code of direct reporting manager. Creates org chart edge."},
    {group:"Reporting Relationships", header:"Functional Head Employee Code", field:"functional_head_code", required:"Optional", help:"Employee code of dotted-line / functional head, if applicable."},
    {group:"Reporting Relationships", header:"HR Partner Employee Code", field:"hr_partner_code", required:"Optional", help:"Employee code of HR partner assigned to this employee."},
    // ── CONFIRMATION / LIFECYCLE ──────────────────────────────────────────
    {group:"Confirmation / Lifecycle", header:"Confirmation Status", field:"confirmation_status", required:"Optional", help:"confirmed / probation / extended / pending_review / overdue / not_applicable."},
    {group:"Confirmation / Lifecycle", header:"Current Confirmation Due Date", field:"current_confirmation_due_date", required:"Optional", help:"Current probation/confirmation due date. Recommended DD-MM-YYYY."},
    {group:"Confirmation / Lifecycle", header:"Standard Probation End Date", field:"standard_probation_end_date", required:"Optional", help:"Standard 6-month probation end date. Recommended DD-MM-YYYY."},
    {group:"Confirmation / Lifecycle", header:"Actual Confirmation Date", field:"actual_confirmation_date", required:"Optional", help:"Date employee was actually confirmed. Recommended DD-MM-YYYY."},
    {group:"Confirmation / Lifecycle", header:"Succession Status", field:"succession_status", required:"Optional", help:"not_assessed / ready_now / ready_12m / external_hire / no_successor."},
    // ── EMERGENCY CONTACTS ────────────────────────────────────────────────
    {group:"Emergency Contacts", header:"Emergency Contact 1 Name", field:"emergency_contact_1_name", required:"Optional", help:"Primary emergency contact full name."},
    {group:"Emergency Contacts", header:"Emergency Contact 1 Relation", field:"emergency_contact_1_relation", required:"Optional", help:"E.g. Spouse, Father, Mother, Sibling."},
    {group:"Emergency Contacts", header:"Emergency Contact 1 Mobile", field:"emergency_contact_1_mobile", required:"Optional", help:"Primary emergency contact mobile."},
    {group:"Emergency Contacts", header:"Emergency Contact 2 Name", field:"emergency_contact_2_name", required:"Optional", help:"Secondary emergency contact full name."},
    {group:"Emergency Contacts", header:"Emergency Contact 2 Relation", field:"emergency_contact_2_relation", required:"Optional", help:"E.g. Sibling, Friend, Parent."},
    {group:"Emergency Contacts", header:"Emergency Contact 2 Mobile", field:"emergency_contact_2_mobile", required:"Optional", help:"Secondary emergency contact mobile."},
    // ── SAFETY ROLES ──────────────────────────────────────────────────────
    {group:"Safety Roles", header:"First Aider", field:"is_first_aider", required:"Optional", help:"TRUE / FALSE. Appears on emergency muster as first aider."},
    {group:"Safety Roles", header:"Fire Marshal", field:"is_fire_marshal", required:"Optional", help:"TRUE / FALSE."},
    {group:"Safety Roles", header:"Field Marshal", field:"is_field_marshal", required:"Optional", help:"TRUE / FALSE."},
    {group:"Safety Roles", header:"Needs Evacuation Help", field:"needs_evacuation_help", required:"Optional", help:"TRUE / FALSE. Flag for mobility-assisted evacuation."},
    // ── STATUTORY / FINANCE ───────────────────────────────────────────────
    {group:"Statutory / Finance", header:"PAN Number", field:"pan_number", required:"Optional", help:"Employee PAN. Stored securely; required for payroll."},
    {group:"Statutory / Finance", header:"UAN Number", field:"uan_number", required:"Optional", help:"Universal Account Number for PF."},
    {group:"Statutory / Finance", header:"ESI Number", field:"esi_number", required:"Optional", help:"ESI registration number if applicable."},
    // ── CONTRACT DETAILS ──────────────────────────────────────────────────
    {group:"Contract Details", header:"Vendor / Contractor Name", field:"vendor_name", required:"Conditional", help:"Required for CONTRACT / CONSULTANT type employees."},
    {group:"Contract Details", header:"Contract Start Date", field:"contract_start_date", required:"Conditional", help:"Recommended DD-MM-YYYY. Required for contract employees."},
    {group:"Contract Details", header:"Contract End Date", field:"contract_end_date", required:"Conditional", help:"Recommended DD-MM-YYYY. Required for contract employees."},
    // ── IMPORT CONTROL ────────────────────────────────────────────────────
    {group:"Import Control", header:"Photo Reference (Leave Blank Normally)", field:"photo_path", required:"No", help:"Leave blank to preserve existing photo. Photo uploads use the app."},
    {group:"Import Control", header:"Photo Action", field:"photo_action", required:"No", help:"KEEP / REPLACE / CLEAR_PHOTO. Default is KEEP."},
    {group:"Import Control", header:"Import Action", field:"import_action", required:"No", help:"UPSERT / UPDATE_ONLY / CREATE_ONLY / SKIP. Default is UPSERT."},
    {group:"Import Control", header:"Row Comments", field:"row_comments", required:"No", help:"HR notes for this row. Ignored by import engine."},
  ];
  const uploadHeaders = professionalEmployeeHeaders.map(h=>h.field);
  const displayHeaders = professionalEmployeeHeaders.map(h=>h.header);
  const professionalToInternalHeader = Object.fromEntries(professionalEmployeeHeaders.map(h=>[h.header,h.field]));
  function normalizeUploadRow(row){
    const out={};
    Object.entries(row||{}).forEach(([k,v])=>{ out[professionalToInternalHeader[k] || k] = v; });
    return out;
  }
  function normalizeHeaderName(v){
    return String(v||"")
      .replace(/\s+/g," ")
      .replace(/\u00A0/g," ")
      .replace(/[._]+/g," ")
      .trim()
      .toLowerCase();
  }
  const headerAliasToField = (() => {
    const m = {};
    professionalEmployeeHeaders.forEach(h => {
      m[normalizeHeaderName(h.header)] = h.field;
      m[normalizeHeaderName(h.field)] = h.field;
    });
    // Legacy / accidental aliases from older templates
    Object.assign(m, {
      "employee code": "employee_code",
      "employee no": "employee_code",
      "employee number": "employee_code",
      "employee id": "employee_code",
      "emp code": "employee_code",
      "emp no": "employee_code",
      "emp id": "employee_code",
      "e code": "employee_code",
      "e-code": "employee_code",
      "ecode": "employee_code",
      "code": "employee_code",
      "d": "employee_code",
      "employee full name": "full_name",
      "full name": "full_name",
      "employment status": "status",
      "status": "status",
      "date of joining": "date_of_joining",
      "doj": "date_of_joining",
      "last working day": "last_working_day",
      "lwd": "last_working_day",
      "site": "site_code",
      "primary site": "site_code",
      "reporting line / directorate": "reporting_manager_code",
      "reporting manager employee code": "reporting_manager_code",
      "reporting manager code": "reporting_manager_code",
      "functional head employee code": "functional_head_code",
      "functional head code": "functional_head_code",
      "hr partner employee code": "hr_partner_code",
      "hr partner code": "hr_partner_code",
      "dob": "date_of_birth",
      "date of birth": "date_of_birth",
      "birth date": "date_of_birth",
      "blood group": "blood_group",
      "blood": "blood_group",
      "gender": "gender",
      "marital status": "marital_status",
      "nationality": "nationality",
      "alternate mobile": "alternate_mobile",
      "alt mobile": "alternate_mobile",
      "emergency contact 1 name": "emergency_contact_1_name",
      "emergency contact 1 relation": "emergency_contact_1_relation",
      "emergency contact 1 mobile": "emergency_contact_1_mobile",
      "emergency contact 2 name": "emergency_contact_2_name",
      "emergency contact 2 relation": "emergency_contact_2_relation",
      "emergency contact 2 mobile": "emergency_contact_2_mobile",
      "ec1 name": "emergency_contact_1_name",
      "ec1 mobile": "emergency_contact_1_mobile",
      "ec2 name": "emergency_contact_2_name",
      "ec2 mobile": "emergency_contact_2_mobile",
      "needs evacuation help": "needs_evacuation_help",
      "evacuation": "needs_evacuation_help",
      "pan": "pan_number",
      "pan number": "pan_number",
      "uan": "uan_number",
      "esi": "esi_number",
      "vendor": "vendor_name",
      "contractor": "vendor_name",
      "contract start": "contract_start_date",
      "contract end": "contract_end_date",
      "standard probation end date": "standard_probation_end_date",
      "probation end date": "standard_probation_end_date",
      "actual confirmation date": "actual_confirmation_date",
      "confirmation date": "actual_confirmation_date",
      "current confirmation due date": "current_confirmation_due_date",
      "due date": "current_confirmation_due_date",
      "probation due date": "current_confirmation_due_date"
    });
    return m;
  })();
  const EMP_MASTER_DATE_FIELDS = [
    "date_of_birth","date_of_joining","last_working_day","probation_end_date","standard_probation_end_date",
    "current_confirmation_due_date","actual_confirmation_date","contract_start_date","contract_end_date"
  ];
  const EMP_MASTER_DATE_FIELD_SET = new Set(EMP_MASTER_DATE_FIELDS);

  function excelSerialToIso(n, wb){
    const num = Number(n);
    if(!Number.isFinite(num) || num < 15000 || num > 90000) return "";
    try{
      const date1904 = !!(wb?.Workbook?.WBProps?.date1904);
      const d = XLSX?.SSF?.parse_date_code ? XLSX.SSF.parse_date_code(num, {date1904}) : null;
      if(d?.y && d?.m && d?.d) return toIsoDateFromParts(d.y, d.m, d.d);
    }catch(e){}
    // fallback for standard Excel 1900 date system
    const epoch = Date.UTC(1899, 11, 30);
    const dt = new Date(epoch + Math.round(num) * 86400000);
    if(Number.isNaN(dt.getTime())) return "";
    return toIsoDateFromParts(dt.getUTCFullYear(), dt.getUTCMonth()+1, dt.getUTCDate());
  }

  function safeCellString(v){
    return v == null ? "" : String(v).trim();
  }

  function isNumericLookingDateString(s){
    return /^\d{1,2}[-\/.]\d{1,2}[-\/.]\d{2,4}$/.test(String(s||"").trim())
      || /^\d{4}[-\/.]\d{1,2}[-\/.]\d{1,2}$/.test(String(s||"").trim());
  }

  function parseExcelDateCell(cell, field, wb){
    const audit = {
      display: cell?.w != null ? String(cell.w).trim() : "",
      raw: cell?.v != null ? String(cell.v).trim() : "",
      type: cell?.t || "",
      format: cell?.z || "",
      parsed_iso: "",
      parse_source: "",
      warning: ""
    };

    if(!cell || (cell.v == null && !audit.display)){
      audit.parse_source = "blank";
      return audit;
    }

    const display = audit.display;
    const raw = cell.v;
    const format = audit.format || "";

    // Prefer unambiguous formatted display such as 05-Dec-2025.
    // This is the target template format and it removes day/month ambiguity.
    if(display && /[A-Za-z]{3,}/.test(display)){
      const iso = parseHrExcelDateToIso(display);
      if(iso && /^\d{4}-\d{2}-\d{2}$/.test(iso)){
        audit.parsed_iso = iso;
        audit.parse_source = "excel_display_dd_mmm";
        return audit;
      }
    }

    // Real Excel date/serial. Use the serial as machine truth and keep display for audit.
    if(typeof raw === "number" && Number.isFinite(raw)){
      const serialIso = excelSerialToIso(raw, wb);
      const displayIso = display ? parseHrExcelDateToIso(display) : "";
      if(serialIso){
        if(field === "date_of_birth"){
          const correctedDob = correctLikelyPost2000DobIso(serialIso);
          const displayLooksPost2000 = displayIso && /^20\d{2}-/.test(displayIso);
          if(correctedDob && (!displayIso || displayLooksPost2000 || isNumericLookingDateString(display))){
            audit.parsed_iso = displayIso && displayLooksPost2000 ? displayIso : correctedDob;
            audit.parse_source = displayIso && displayLooksPost2000 ? "excel_display_post_2000_dob_guard" : "excel_serial_post_2000_dob_guard";
            audit.warning = `DOB serial/raw produced ${serialIso}, which creates implausible age. Applied post-2000 two-digit-year guard as ${audit.parsed_iso}.`;
            return audit;
          }
        }
        audit.parsed_iso = serialIso;
        audit.parse_source = "excel_serial";
        if(display && displayIso && displayIso !== serialIso && isNumericLookingDateString(display)){
          audit.warning = `Excel display parses as ${displayIso}, but serial/raw date is ${serialIso}. Check workbook date format; system used serial.`;
        }
        return audit;
      }
    }

    // Actual Date object when SheetJS returns cellDates.
    if(raw instanceof Date && !Number.isNaN(raw.getTime())){
      const iso = toIsoDateFromParts(raw.getFullYear(), raw.getMonth()+1, raw.getDate());
      audit.parsed_iso = iso;
      audit.parse_source = "excel_date_object";
      return audit;
    }

    // Text / string cells. Approved deterministic formats only.
    const candidate = display || safeCellString(raw);
    const iso = parseHrExcelDateToIso(candidate);
    if(field === "date_of_birth" && iso){
      const correctedDob = correctLikelyPost2000DobIso(iso);
      if(correctedDob){
        audit.parsed_iso = correctedDob;
        audit.parse_source = "text_post_2000_dob_guard";
        audit.warning = `DOB parsed as ${iso}, which creates implausible age. Applied post-2000 two-digit-year guard as ${correctedDob}.`;
        return audit;
      }
    }
    audit.parsed_iso = iso;
    audit.parse_source = candidate && iso ? "text_deterministic" : "unreadable";
    if(candidate && isNumericLookingDateString(candidate)){
      audit.warning = "Numeric-looking text date was parsed using Indian HR DD-MM rule. Prefer real Excel date cell displayed as DD-MMM-YYYY.";
    }
    return audit;
  }

  function cellDisplayValue(cell, field, wb){
    if(!cell) return "";
    if(EMP_MASTER_DATE_FIELD_SET.has(field)){
      return parseExcelDateCell(cell, field, wb).parsed_iso || "";
    }
    if(cell.w != null && String(cell.w).trim() !== "") return String(cell.w).trim();
    if(cell.v == null) return "";
    return String(cell.v).trim();
  }

  function worksheetToProfessionalRows(ws, wb){
    const range = XLSX.utils.decode_range(ws["!ref"] || "A1:A1");
    const headerRow = range.s.r;
    const headers = [];
    for(let c=range.s.c; c<=range.e.c; c++){
      const addr = XLSX.utils.encode_cell({r: headerRow, c});
      const raw = cellDisplayValue(ws[addr], "", wb);
      const field = headerAliasToField[normalizeHeaderName(raw)] || null;
      headers.push({c, raw, field});
    }
    const unrecognizedHeaders = headers.filter(h=>h.raw && !h.field).map(h=>h.raw).slice(0,8);
    if(unrecognizedHeaders.length) console.warn("Unrecognized Employee Master headers", unrecognizedHeaders);
    const out = [];
    for(let r=headerRow+1; r<=range.e.r; r++){
      const obj = {};
      const dateAudit = {};
      let hasAny = false;
      headers.forEach(h=>{
        if(!h.field) return;
        const addr = XLSX.utils.encode_cell({r, c:h.c});
        const cell = ws[addr];
        let val = "";
        if(EMP_MASTER_DATE_FIELD_SET.has(h.field)){
          const audit = parseExcelDateCell(cell, h.field, wb);
          dateAudit[h.field] = audit;
          val = audit.parsed_iso || "";
        }else{
          val = cellDisplayValue(cell, h.field, wb);
        }
        if(String(val||"").trim() !== "") hasAny = true;
        obj[h.field] = val;
      });
      if(Object.keys(dateAudit).length) obj._date_audit = dateAudit;
      if(hasAny) out.push(obj);
    }
    return out;
  }

  function toDisplayRow(obj){
    const o={};
    professionalEmployeeHeaders.forEach(h=>{ o[h.header]=obj?.[h.field] ?? ""; });
    return o;
  }

  function applyDateColumnFormats(ws, headers){
    if(!ws || !ws["!ref"]) return;
    const range = XLSX.utils.decode_range(ws["!ref"]);
    headers.forEach((header, idx)=>{
      const field = professionalToInternalHeader[header] || headerAliasToField[normalizeHeaderName(header)] || header;
      if(!EMP_MASTER_DATE_FIELD_SET.has(field)) return;
      for(let r=range.s.r+1; r<=range.e.r; r++){
        const addr = XLSX.utils.encode_cell({r, c:idx});
        const cell = ws[addr];
        if(!cell || cell.v == null || cell.v === "") continue;
        // Convert ISO strings to real Excel Date cells in generated workbooks.
        if(typeof cell.v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(cell.v)){
          cell.v = isoToDateObject(cell.v);
          cell.t = "d";
        }
        cell.z = "dd-mmm-yyyy";
      }
    });
  }

  async function downloadGeneratedTemplate(){
    try{
      if(!window.XLSX) throw new Error("Excel generator not loaded. Refresh once and try again.");

      // Pull current tenant setup values so the workbook is not hard-coded.
      const [sitesRes, orgRes, empRes, gradeRes] = await Promise.all([
        supabase.from("sites").select("site_code,site_name").order("site_code"),
        supabase.from("v_org_units_admin").select("org_unit_code,org_unit_name,org_unit_type,parent_org_unit_code").order("display_order"),
        supabase.from("employees").select("employee_code,full_name,status").order("employee_code"),
        supabase.from("grade_designation_matrix").select("*").limit(500)
      ]);

      const siteCodes=[...(sitesRes.data||[]).map(s=>s.site_code).filter(Boolean)];
      if(!siteCodes.includes("P77")) siteCodes.push("P77");
      if(!siteCodes.includes("P79")) siteCodes.push("P79");

      const orgUnits=(orgRes.data||[]).filter(o=>o.org_unit_code);
      const employeeCodes=(empRes.data||[]).filter(e=>e.employee_code).map(e=>String(e.employee_code));
      const employeeLookup=(empRes.data||[]).filter(e=>e.employee_code).map(e=>`${e.employee_code} - ${e.full_name||""}`);
      const gradeRows=gradeRes.data||[];
      const grades=[...new Set(gradeRows.map(r=>r.grade).filter(Boolean))];
      const bands=[...new Set(gradeRows.map(r=>r.band).filter(Boolean))];
      const designationNames=[...new Set(gradeRows.map(r=>r.business_enablement_designations||r.science_led_designations||r.designation_name||r.designation||"").filter(Boolean))];
      const tracks=["Business Enablement","Science Led","Engineering","Technical Services","Lab Support","Other"];

      const wb=XLSX.utils.book_new();

      const instructions=[
        ["Employee Master Upload Template"],
        ["Purpose","Use this workbook to correct employee master data safely."],
        ["System Rule","The system reads ONLY the sheet named: Employee Master Upload. Professional HR headers are mapped internally by the system."],
        ["Setup Driven","Dropdown/reference values are generated from current Setup Data and employee master where available."],
        ["Photo Rule","Blank photo_path or photo_action=KEEP preserves existing photos. CLEAR_PHOTO removes a photo only when explicitly used."],
        ["Date Format","Use real Excel Date cells displayed as DD-MMM-YYYY. Example: 05-Dec-2025. Avoid text dates such as 05-12-25."],
        ["Manager Fields","Reporting Manager / Functional Head / HR Partner must use employee codes. The manager may belong to a different department."],
        ["Site Rule","77 maps to P77, 79 maps to P79. NA/blank means unassigned unless Setup Data defines NA as an actual site."],
        ["Nulls","Do not type null. Leave blank if unknown."],
        ["Upload Flow","Fill Employee Master Upload sheet → upload → validate → review errors/warnings → commit."],
        ["Do Not Edit","Do not rename the Employee Master Upload sheet. Keep the professional header names unchanged."]
      ];
      const instWs=XLSX.utils.aoa_to_sheet(instructions);
      instWs["!cols"]=[{wch:24},{wch:115}];
      XLSX.utils.book_append_sheet(wb,instWs,"Instructions");

      const sample={
        employee_code:"1001",
        full_name:"Sample Employee",
        status:"active",
        date_of_joining:isoToDateObject("2026-05-03"),
        last_working_day:"",
        site_code:siteCodes[0]||"P77",
        division_code:"",
        department_code:orgUnits.find(o=>o.org_unit_type==="department")?.org_unit_code||"",
        sub_department_code:"",
        team_cell_code:"",
        org_chart_node_code:"",
        designation_name:designationNames[0]||"Executive",
        designation_track:tracks[0],
        band:bands[0]||"",
        grade:grades[0]||"8-II",
        employee_type_code:"ONROLL",
        reporting_manager_code:employeeCodes[0]||"",
        functional_head_code:"",
        hr_partner_code:"",
        official_email:"sample.employee@company.com",
        personal_email:"",
        personal_mobile:"9999999999",
        confirmation_status:"probation",
        probation_end_date:isoToDateObject("2026-11-03"),
        succession_status:"not_assessed",
        gate_access_enabled:"TRUE",
        self_punch_enabled:"TRUE",
        is_first_aider:"FALSE",
        is_fire_marshal:"FALSE",
        is_field_marshal:"FALSE",
        photo_path:"",
        photo_action:"KEEP",
        import_action:"UPSERT",
        row_comments:"Sample row. Delete before live upload."
      };
      const dataRows=[toDisplayRow(sample)];
      for(let i=0;i<300;i++){
        const blank={}; displayHeaders.forEach(h=>blank[h]=""); blank["Photo Action"]="KEEP"; blank["Import Action"]="UPSERT"; dataRows.push(blank);
      }
      const dataWs=XLSX.utils.json_to_sheet(dataRows,{header:displayHeaders, cellDates:true});
      applyDateColumnFormats(dataWs, displayHeaders);
      dataWs["!cols"]=displayHeaders.map(h=>({wch:Math.max(16,Math.min(38,h.length+5))}));
      XLSX.utils.book_append_sheet(wb,dataWs,REQUIRED_SHEET);

      const fieldRows=professionalEmployeeHeaders.map(h=>({"Excel Header":h.header,"Section":h.group,"Required":h.required,"What It Means":h.help,"System Field":h.field}));
      const dictWs=XLSX.utils.json_to_sheet(fieldRows);
      dictWs["!cols"]=[{wch:38},{wch:30},{wch:14},{wch:100},{wch:34}];
      XLSX.utils.book_append_sheet(wb,dictWs,"Header Dictionary");

      const maxLen=Math.max(8,siteCodes.length,orgUnits.length,employeeLookup.length,grades.length,bands.length,designationNames.length);
      const listRows=[];
      listRows.push(["status","employee_type_code","confirmation_status","succession_status","boolean","photo_action","import_action","site_code","org_unit_code","employee_code_lookup","grade","band","designation_name","designation_track"]);
      for(let i=0;i<maxLen;i++){
        listRows.push([
          ["active","paused","exited"][i]||"",
          ["ONROLL","CONTRACT","TRAINEE","INTERN","CONSULTANT","VENDOR"][i]||"",
          ["confirmed","probation","scheduled","unconfirmed","overdue","not_applicable","unknown"][i]||"",
          ["ready_now","ready_6_12","no_successor","external_hire","not_assessed"][i]||"",
          ["TRUE","FALSE"][i]||"",
          ["KEEP","REPLACE","CLEAR_PHOTO"][i]||"",
          ["UPSERT","UPDATE_ONLY","CREATE_ONLY","SKIP"][i]||"",
          siteCodes[i]||"",
          orgUnits[i]?`${orgUnits[i].org_unit_code} - ${orgUnits[i].org_unit_name}`:"",
          employeeLookup[i]||"",
          grades[i]||"",
          bands[i]||"",
          designationNames[i]||"",
          tracks[i]||""
        ]);
      }
      const listWs=XLSX.utils.aoa_to_sheet(listRows);
      listWs["!cols"]=listRows[0].map(()=>({wch:28}));
      XLSX.utils.book_append_sheet(wb,listWs,"Lists From Setup");

      const setupNote=[
        ["Setup Data Dependency"],
        ["Sites",siteCodes.length ? siteCodes.join(", ") : "No sites found; default P77/P79 included."],
        ["Org Units",`${orgUnits.length} org units loaded from Setup Data.`],
        ["Employee Codes",`${employeeLookup.length} employees loaded for manager-code reference.`],
        ["Grade Matrix",`${gradeRows.length} grade/designation rows loaded where available.`],
        ["Succession", "succession_status is only a simple employee-master flag. A proper Succession Plan module should manage readiness, successor names, risk, review date, and role criticality separately."]
      ];
      const setupWs=XLSX.utils.aoa_to_sheet(setupNote);
      setupWs["!cols"]=[{wch:24},{wch:120}];
      XLSX.utils.book_append_sheet(wb,setupWs,"Setup Source Notes");

      XLSX.writeFile(wb,`employee_master_upload_template_${new Date().toISOString().slice(0,10)}.xlsx`);
    }catch(err){
      setUploadErr(err.message||String(err));
    }
  }

  // v1.8.30 deterministic frontend date parser
  // Target file standard:
  // - Real Excel date cells displayed as DD-MMM-YYYY, e.g. 05-Dec-2025.
  // - XLSX importer captures display/raw/type/format before staging.
  // - Store/send to Supabase as ISO YYYY-MM-DD.
  // - Never use browser Date.parse/new Date for numeric-looking strings.
  function padDate2(n){
    return String(n).padStart(2,"0");
  }
  function isBlankDateValue(value){
    if(value == null) return true;
    const s = String(value).trim().toLowerCase();
    return !s || ["null","na","n/a","-","none","undefined"].includes(s);
  }
  function toIsoDateFromParts(year, month, day){
    const y = Number(year), m = Number(month), d = Number(day);
    if(!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return "";
    if(y < 1900 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) return "";
    const dt = new Date(Date.UTC(y, m - 1, d));
    if(dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) return "";
    return `${y}-${padDate2(m)}-${padDate2(d)}`;
  }

  function isoParts(iso){
    const m = String(iso||"").slice(0,10).match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if(!m) return null;
    return {y:Number(m[1]), m:Number(m[2]), d:Number(m[3])};
  }
  function ageYearsOnIso(iso, asOf=new Date()){
    const p = isoParts(iso);
    if(!p) return null;
    let age = asOf.getFullYear() - p.y;
    const mm = asOf.getMonth()+1;
    const dd = asOf.getDate();
    if(mm < p.m || (mm === p.m && dd < p.d)) age--;
    return age;
  }
  function correctLikelyPost2000DobIso(iso){
    const p = isoParts(iso);
    if(!p) return "";
    if(p.y < 1900 || p.y > 1929) return "";
    const corrected = toIsoDateFromParts(p.y + 100, p.m, p.d);
    const correctedAge = ageYearsOnIso(corrected);
    if(corrected && correctedAge != null && correctedAge >= 14 && correctedAge <= 35) return corrected;
    return "";
  }
  function monthNameToNumber(mon){
    const m = String(mon||"").trim().toLowerCase().slice(0,3);
    return {jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12}[m] || 0;
  }
  function parseHrExcelDateToIso(value){
    if(isBlankDateValue(value)) return "";

    // Actual JS Date object from SheetJS when cellDates:true
    if(value instanceof Date && !isNaN(value.getTime())){
      return toIsoDateFromParts(value.getFullYear(), value.getMonth()+1, value.getDate());
    }

    // Excel serial number
    if(typeof value === "number" && Number.isFinite(value)){
      return excelSerialToIso(value) || "";
    }

    let s = String(value).trim();
    if(isBlankDateValue(s)) return "";
    s = s.replace(/\s+/g," ").replace(/,/g,"");

    // DD-MMM-YYYY / DD MMM YYYY / DD-MMM-YY
    // This is the preferred upload display format because month name is unambiguous.
    let m = s.match(/^(\d{1,2})[-\s\/.]([A-Za-z]{3,})[-\s\/.](\d{2,4})$/);
    if(m){
      const day = Number(m[1]);
      const month = monthNameToNumber(m[2]);
      let year = Number(m[3]);
      if(year < 100) year = year >= 70 ? 1900 + year : 2000 + year;
      return toIsoDateFromParts(year, month, day);
    }

    // ISO YYYY-MM-DD / YYYY/MM/DD / YYYY.MM.DD.
    // Also repair old damaged value YYYY-DD-MM only when the middle part > 12, e.g. 1985-13-05.
    m = s.match(/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})$/);
    if(m){
      const y = Number(m[1]), a = Number(m[2]), b = Number(m[3]);
      if(a >= 1 && a <= 12) return toIsoDateFromParts(y, a, b);
      if(a >= 13 && a <= 31 && b >= 1 && b <= 12) return toIsoDateFromParts(y, b, a);
      return "";
    }

    // Indian HR format DD-MM-YYYY / DD/MM/YYYY / DD.MM.YYYY
    m = s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{4})$/);
    if(m){
      return toIsoDateFromParts(Number(m[3]), Number(m[2]), Number(m[1]));
    }

    // Indian HR format DD-MM-YY / DD/MM/YY / DD.MM.YY
    m = s.match(/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{2})$/);
    if(m){
      const yy = Number(m[3]);
      const yyyy = yy >= 70 ? 1900 + yy : 2000 + yy;
      return toIsoDateFromParts(yyyy, Number(m[2]), Number(m[1]));
    }

    // Reject unreadable dates instead of guessing.
    // Do not call Date.parse/new Date for text like 05-12-2025.
    return "";
  }
  // Keep old function name for existing upload flow.
  function excelDateToIso(v){
    return parseHrExcelDateToIso(v);
  }
  function isoToDdMmmYyyy(v){
    if(!v)return "";
    const s=String(v).slice(0,10);
    const m=s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if(!m)return s;
    const months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    return `${m[3]}-${months[Number(m[2])-1]}-${m[1]}`;
  }
  function isoToDateObject(v){
    if(!v)return "";
    const s=String(v).slice(0,10);
    const m=s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if(!m)return "";
    return new Date(Date.UTC(Number(m[1]), Number(m[2])-1, Number(m[3])));
  }
  function boolText(v){
    if(v===true || String(v).toUpperCase()==="TRUE")return "TRUE";
    if(v===false || String(v).toUpperCase()==="FALSE")return "FALSE";
    return v==null?"":String(v).trim();
  }
  function cleanRow(r){
    const out={};
    uploadHeaders.forEach(h=>{
      let v=r[h];
      if(EMP_MASTER_DATE_FIELD_SET.has(h)) v=excelDateToIso(v);
      else if(["gate_access_enabled","self_punch_enabled","is_first_aider","is_fire_marshal","is_field_marshal"].includes(h)) v=boolText(v);
      else v = v==null ? "" : String(v).trim();
      if(String(v).toLowerCase()==="null") v="";
      out[h]=v;
    });
    if(r?._date_audit) out._date_audit = r._date_audit;
    if(!out.photo_action) out.photo_action="KEEP";
    if(!out.import_action) out.import_action="UPSERT";
    return out;
  }
  function parseWorkbook(arrayBuffer){
    if(!window.XLSX) throw new Error("Excel parser not loaded. Please refresh once and try again.");
    const wb = XLSX.read(arrayBuffer,{type:"array",cellDates:true,cellNF:true,cellText:true});
    if(!wb.SheetNames.includes(REQUIRED_SHEET)){
      throw new Error(`Sheet "${REQUIRED_SHEET}" not found. Do not rename the data sheet. Found: ${wb.SheetNames.join(", ")}`);
    }
    const ws = wb.Sheets[REQUIRED_SHEET];
    const json = worksheetToProfessionalRows(ws, wb);
    const cleaned=json.map(normalizeUploadRow).map(cleanRow).filter(r=>Object.values(r).some(v=>String(v||"").trim()!==""));
    const localWarnings=["Dates are read from the actual Excel cell object. Preferred template format is real Excel Date cells displayed as DD-MMM-YYYY. Date audit is preserved in staging for review."];
    if(cleaned.some(r=>r.photo_path && r.photo_action==="KEEP")) localWarnings.push("Some rows have photo_path but photo_action=KEEP. Existing photos will be preserved.");
    setWarnings(localWarnings);
    return cleaned;
  }
  async function onExcelFile(e){
    const f=e.target.files?.[0]; if(!f)return;
    setFileName(f.name); setBusy(true); setResult(null); setBatch(null);
    try{
      const buf=await f.arrayBuffer();
      const parsed=parseWorkbook(buf);
      setRows(parsed);
      if(!parsed.length) setUploadErr("No usable rows found in Employee Master Upload sheet.");
    }catch(err){
      setRows([]);
      setUploadErr(err.message||String(err));
    }finally{setBusy(false)}
  }

  async function downloadCurrentWorkbook(){
    setBusy(true);
    try{
      if(!window.XLSX) throw new Error("Excel parser not loaded. Refresh and try again.");
      const {data,error}=await supabase.from("employees").select(uploadHeaders.filter(h=>!["photo_action","import_action","row_comments"].includes(h)).join(",")).order("employee_code");
      if(error)throw error;
      const safe=(data||[]).map(r=>{
        const o={};
        uploadHeaders.forEach(h=>{
          if(h==="photo_path") o[h]=""; // do not expose backend photo paths in HR correction workbook
          else if(h==="photo_action") o[h]="KEEP";
          else if(h==="import_action") o[h]="UPDATE_ONLY";
          else if(h==="row_comments") o[h]="Downloaded from system";
          else if(EMP_MASTER_DATE_FIELD_SET.has(h)) o[h]=isoToDateObject(r[h]);
          else o[h]=r[h]??"";
        });
        return o;
      });
      const wb=XLSX.utils.book_new();
      const inst=[
        ["Employee Master Download / Correction Workbook"],
        ["System reads only sheet named Employee Master Upload."],
        ["Do not rename the Employee Master Upload sheet or professional headers."],
        ["Photo Reference should normally be blank. Blank preserves existing photo."],
        ["Use photo_action CLEAR_PHOTO only to remove a photo."],
        ["Recommended date format: real Excel Date cells displayed as DD-MMM-YYYY. Date audit is captured during upload."]
      ];
      XLSX.utils.book_append_sheet(wb,XLSX.utils.aoa_to_sheet(inst),"Instructions");
      const curWs=XLSX.utils.json_to_sheet(safe.map(toDisplayRow),{header:displayHeaders, cellDates:true});
      applyDateColumnFormats(curWs, displayHeaders);
      XLSX.utils.book_append_sheet(wb,curWs,REQUIRED_SHEET);
      const dict=professionalEmployeeHeaders.map(h=>({"Excel Header":h.header,"Section":h.group,"Required":h.required,"What It Means":h.help,"System Field":h.field}));
      XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet(dict),"Header Dictionary");
      XLSX.writeFile(wb,`employee_master_current_${new Date().toISOString().slice(0,10)}.xlsx`);
    }catch(err){setUploadErr(err.message||String(err));}
    finally{setBusy(false)}
  }

  async function stageAndValidate(){
    if(!rows.length){setUploadErr("Choose Employee Master Excel first.");return}
    setBusy(true); setResult(null); setBatch(null);
    try{
      const {data,error}=await supabase.rpc("rpc_stage_employee_master_import",{
        p_file_name:fileName||"employee_master_upload.xlsx",
        p_source_sheet_name:REQUIRED_SHEET,
        p_rows:rows
      });
      if(error)throw error;
      if(data?.ok===false)throw new Error(data?.error||"Stage failed");

      const batchId=data?.batch_id;
      const {data:batchRows,error:bre}=await supabase
        .from("v_employee_master_import_batch_summary")
        .select("*")
        .eq("batch_id",batchId)
        .single();
      if(bre)throw bre;

      const {data:rr,error:rre}=await supabase
        .from("employee_master_import_rows")
        .select("*")
        .eq("batch_id",batchId)
        .order("row_number");
      if(rre)throw rre;

      setBatch(batchRows||{batch_id:batchId, id:batchId, ...data});
      setResult(rr||[]);
    }catch(err){
      setUploadErr(err.message||String(err));
    }finally{setBusy(false)}
  }

  async function commitBatch(){
    const batchId=batch?.batch_id || batch?.id;
    if(!batchId){setUploadErr("Validate first.");return}
    const errors=(result||[]).filter(r=>r.validation_status==="error");
    if(errors.length){setUploadErr("Cannot commit. Resolve validation errors first.");return}
    if(!confirm("Apply Employee Master changes now by employee_code? Master fields in the upload will be applied; existing photos are preserved unless Photo Action is CLEAR_PHOTO or REPLACE."))return;
    setBusy(true);
    try{
      const {data,error}=await supabase.rpc("rpc_apply_employee_master_batch_safe",{p_batch_id:batchId});
      if(error)throw error;
      if(data?.ok===false && (data?.failed||0)>0){
        setUploadSuccess(`Commit completed with failures. Created: ${data?.created||0}, Updated: ${data?.updated||0}, Skipped: ${data?.skipped||0}, Failed: ${data?.failed||0}`);;
      }else{
        setUploadSuccess(`Committed. Created: ${data?.created||0}, Updated: ${data?.updated||0}, Skipped: ${data?.skipped||0}, Failed: ${data?.failed||0}`);;
      }

      const {data:batchRows,error:bre}=await supabase
        .from("v_employee_master_import_batch_summary")
        .select("*")
        .eq("batch_id",batchId)
        .single();
      if(!bre)setBatch(batchRows);

      const {data:rr,error:rre}=await supabase
        .from("employee_master_import_rows")
        .select("*")
        .eq("batch_id",batchId)
        .order("row_number");
      if(!rre)setResult(rr||[]);
    }catch(err){setUploadErr(err.message||String(err));}
    finally{setBusy(false)}
  }

  const shown=(result||[]).filter(r=>{
    if(previewMode==="errors")return r.validation_status==="error";
    if(previewMode==="warnings")return r.validation_status==="warning";
    return true;
  });
  const summary=result?{
    total:result.length,
    error:result.filter(r=>r.validation_status==="error").length,
    warning:result.filter(r=>r.validation_status==="warning").length,
    valid:result.filter(r=>r.validation_status==="valid").length,
    committed:result.filter(r=>r.validation_status==="committed").length,
    skipped:result.filter(r=>r.validation_status==="skipped"||r.commit_status==="skipped").length,
    failed:result.filter(r=>r.validation_status==="failed"||r.commit_status==="failed").length,
    created:result.filter(r=>r.commit_status==="created").length,
    updated:result.filter(r=>r.commit_status==="updated").length
  }:null;

  const [showDict,setShowDict]=useState(false);
  const [uploadErr,setUploadErr]=useState("");
  const [uploadSuccess,setUploadSuccess]=useState("");
  const groups=[...new Set(professionalEmployeeHeaders.map(h=>h.group))];
  const requiredCols=professionalEmployeeHeaders.filter(h=>h.required==="Yes"||h.required==="Recommended");

  return <div className="grid emp-upload-v2">
    <div className="card premium">
      <div className="cardTitle">
        <div>
          <h3 style={{margin:0}}>Employee Master Excel Upload</h3>
          <div className="muted" style={{marginTop:4}}>{professionalEmployeeHeaders.length} supported columns across {groups.length} groups · Sheet must be named <b>Employee Master Upload</b></div>
        </div>
        <div style={{display:'flex',gap:8}}>
          <span className="badge goldb">Safe XLSX</span>
          <button className="btn ghost small" onClick={()=>setShowDict(!showDict)}>{showDict?'Hide':'Show'} Column Dictionary</button>
        </div>
      </div>

      {/* Rules strip */}
      <div className="upload-rules">
        <div><b>Photo safe</b><span>Blank photo_path keeps existing photo unchanged.</span></div>
        <div><b>Date format</b><span>Use real Excel Date cells displayed as DD-MMM-YYYY; upload preserves raw/display/parsed audit.</span></div>
        <div><b>Manager codes</b><span>Reporting / functional / HR partner fields need employee codes, not names.</span></div>
        <div><b>New fields</b><span>DOB, blood group, emergency contacts, statutory numbers now supported.</span></div>
      </div>

      {/* Column dictionary */}
      {showDict&&<div style={{marginTop:14}}>
        {groups.map(grp=>{
          const cols=professionalEmployeeHeaders.filter(h=>h.group===grp);
          return <div key={grp} style={{marginBottom:16}}>
            <div style={{fontWeight:800,fontSize:11,textTransform:'uppercase',letterSpacing:'.10em',color:'var(--t2)',marginBottom:8}}>{grp}</div>
            <div style={{display:'grid',gap:6}}>
              {cols.map(h=><div key={h.field} style={{display:'grid',gridTemplateColumns:'220px 80px 1fr',gap:10,alignItems:'start',padding:'8px 10px',background:'var(--card2)',borderRadius:10,border:'1px solid var(--b0)'}}>
                <div><b style={{fontSize:12.5,color:'var(--t0)'}}>{h.header}</b><div className="muted" style={{fontSize:10,marginTop:2,fontFamily:'monospace'}}>{h.field}</div></div>
                <span className={cn("badge",h.required==="Yes"?"redb":h.required==="Recommended"?"amber":h.required==="Conditional"?"goldb":"muted")} style={{alignSelf:'start'}}>{h.required}</span>
                <span style={{fontSize:12,color:'var(--t2)',lineHeight:1.5}}>{h.help}</span>
              </div>)}
            </div>
          </div>;
        })}
      </div>}

      <div className="toolbar" style={{marginTop:14}}>
        <button className="btn ghost" type="button" onClick={downloadGeneratedTemplate}>⬇ Generate Template</button>
        <button className="btn ghost" disabled={busy} onClick={downloadCurrentWorkbook}>⬇ Current Master Excel</button>
        <input className="input" type="file" accept=".xlsx,.xls" onChange={onExcelFile} style={{maxWidth:260}}/>
        <button className="btn gold" disabled={busy||!rows.length} onClick={stageAndValidate}>{busy?"Working…":"Stage & Validate"}</button>
        <button className="btn danger" disabled={busy||!result?.length||summary?.error>0} onClick={commitBatch}>Commit Validated Upload</button>
      </div>
      {warnings.length>0&&<div className="warning">{warnings.join(" ")}</div>}
      {uploadErr&&<div className="error" style={{marginTop:8,padding:"8px 12px",background:"var(--red-t)",borderRadius:10}}>{uploadErr} <button onClick={()=>setUploadErr("")} style={{marginLeft:8,border:0,background:"transparent",cursor:"pointer",color:"var(--red)"}}>✕</button></div>}
      {uploadSuccess&&<div className="notice good">{uploadSuccess} <button onClick={()=>setUploadSuccess("")} style={{marginLeft:8,border:0,background:"transparent",cursor:"pointer"}}>✕</button></div>}
      {rows.length>0&&!result&&<div className="warning">Loaded <b>{rows.length}</b> row(s) from <b>{REQUIRED_SHEET}</b>. Preview below — no database changes yet.</div>}
      {batch&&<div className="notice good">Batch created: <b>{batch.batch_id||batch.id}</b> · Status: <b>{batch.status||"draft"}</b> · Rows: <b>{batch.total_rows||rows.length}</b></div>}
    </div>

    {rows.length>0&&!result&&<div className="table emp-master-grid"><table><thead><tr>{uploadHeaders.map(k=><th key={k}>{humanLabel(k)}</th>)}</tr></thead><tbody>{rows.slice(0,30).map((r,i)=><tr key={i}>{uploadHeaders.map(k=><td key={k}>{String(r[k]??"")}</td>)}</tr>)}</tbody></table></div>}

    {summary&&<div className="card premium">
      <div className="cardTitle"><h3>Validation Summary</h3><span className="badge">{summary.total}</span></div>
      <div className="upload-summary">
        <div><b>{summary.valid}</b><span>Valid</span></div>
        <div><b>{summary.warning}</b><span>Warnings</span></div>
        <div><b>{summary.error}</b><span>Errors</span></div>
        <div><b>{summary.committed}</b><span>Committed</span></div>
        <div><b>{summary.created}</b><span>Created</span></div>
        <div><b>{summary.updated}</b><span>Updated</span></div>
        <div><b>{summary.skipped}</b><span>Skipped</span></div>
        <div><b>{summary.failed}</b><span>Failed</span></div>
      </div>
      <div className="toolbar">
        <button className={cn("btn",previewMode==="all"?"gold":"ghost")} onClick={()=>setPreviewMode("all")}>All</button>
        <button className={cn("btn",previewMode==="errors"?"gold":"ghost")} onClick={()=>setPreviewMode("errors")}>Errors</button>
        <button className={cn("btn",previewMode==="warnings"?"gold":"ghost")} onClick={()=>setPreviewMode("warnings")}>Warnings</button>
      </div>
      <div className="table"><table><thead><tr><th>Row</th><th>Employee Code</th><th>Validation</th><th>Commit</th><th>Messages</th><th>Photo Action</th><th>Import Action</th></tr></thead><tbody>{shown.map(r=><tr key={r.id}><td>{r.row_number}</td><td>{r.employee_code}</td><td><span className={cn("badge",r.validation_status==="error"?"critical":r.validation_status==="warning"?"amber":r.validation_status==="valid"||r.validation_status==="committed"?"green":"muted")}>{r.validation_status}</span></td><td>{r.commit_status||"not_committed"}</td><td>{r.commit_message||((r.validation_messages||[]).join("; ")||"Ready")}</td><td>{r.row_payload?.photo_action||"KEEP"}</td><td>{r.import_action}</td></tr>)}</tbody></table></div>
    </div>}
  </div>
}

function UsersMgmt(){
  const [q,setQ]=useState("");
  const [selected,setSelected]=useState(null);
  const [form,setForm]=useState({});
  const [msg,setMsg]=useState("");
  const [busy,setBusy]=useState(false);
  const {data,loading,error,reload}=useLoad(async()=>{
    const [usersRes,empsRes,rolesRes,linksRes]=await Promise.all([
      supabase.from("user_accounts").select("*").order("created_at",{ascending:false}),
      supabase.from("employees").select("id,tenant_id,employee_code,full_name,status,official_email,personal_email,department_code,site_code,designation_name,date_of_birth,date_of_joining,employee_type_code").order("employee_code"),
      supabase.from("user_roles").select("*").eq("is_active",true),
      supabase.from("employee_user_links").select("*")
    ]);
    if(usersRes.error)throw usersRes.error;
    if(empsRes.error)throw empsRes.error;
    const allEmployees=empsRes.data||[];
    const empRows=allEmployees.filter(e=>isActiveEmployeeStatus(e.status));
    const empMap=Object.fromEntries(allEmployees.map(e=>[e.id,e]));
    const linksByUser={};
    (linksRes.data||[]).forEach(l=>{ if(l.user_id && l.is_primary!==false) linksByUser[l.user_id]=l; });
    const rolesByUser={};
    (rolesRes.data||[]).forEach(r=>{rolesByUser[r.user_id]=rolesByUser[r.user_id]||[];rolesByUser[r.user_id].push(r.role)});
    const users=(usersRes.data||[]).map(u=>{
      const linkedId=u.employee_id || linksByUser[u.auth_user_id]?.employee_id || null;
      const employee=empMap[linkedId]||null;
      return {...u,linked_employee_id:linkedId,employee,roles:rolesByUser[u.auth_user_id]||rolesByUser[u.user_id]||[]};
    });
    return {users,employees:empRows,allEmployees,links:linksRes.data||[]};
  },[]);
  if(loading)return <Loading label="User Management"/>;
  if(error)return <div className="card premium"><h3>User Management failed</h3><p className="error">{error.message||String(error)}</p></div>;
  const portalOptions=["hr","guard","employee","manager"];
  const roleOptions=["tenant_admin","hr_admin","hr_team","manager","guard","employee"];
  const accountStatuses=["pending_setup","active","paused","deactivated","locked","exited","password_reset_required"];
  const rows=(data.users||[]).filter(u=>`${u.username||''} ${u.login_identifier||''} ${u.display_name||''} ${u.employee?.full_name||''} ${u.employee?.employee_code||''}`.toLowerCase().includes(q.toLowerCase()));
  const unlinked=rows.filter(u=>!u.linked_employee_id);
  function scoreEmployeeForUser(u,e){
    const hay=`${u.username||''} ${u.login_identifier||''} ${u.display_name||''}`.toLowerCase().replace(/[^a-z0-9]+/g,' ');
    const email=`${e.official_email||''} ${e.personal_email||''}`.toLowerCase();
    const name=String(e.full_name||'').toLowerCase();
    const code=String(e.employee_code||'').toLowerCase();
    let s=0;
    if(hay.includes(code) && code) s+=20;
    name.split(/\s+/).filter(Boolean).forEach(part=>{ if(part.length>2 && hay.includes(part)) s+=8; });
    if(email && (`${u.login_identifier||''} ${u.username||''}`.toLowerCase().includes(email.split('@')[0]||'__'))) s+=12;
    if(String(u.display_name||'').toLowerCase().includes(name) && name) s+=20;
    return s;
  }
  function suggestionsFor(u){
    return (data.employees||[]).map(e=>({...e,_score:scoreEmployeeForUser(u,e)})).filter(e=>e._score>0).sort((a,b)=>b._score-a._score).slice(0,5);
  }
  function openUser(u){
    setSelected(u); setMsg("");
    setForm({employee_id:u.linked_employee_id||u.employee_id||"",account_status:u.account_status||"active",portal_access:u.portal_access||[],roles:u.roles||[]});
  }
  function toggleArr(key,val){const arr=new Set(form[key]||[]); arr.has(val)?arr.delete(val):arr.add(val); setForm({...form,[key]:[...arr]});}
  function applyRohan(){
    const e=(data.employees||[]).find(x=>String(x.employee_code)==='520' || /rohan\s+puri/i.test(x.full_name||''));
    if(e) setForm({...form,employee_id:e.id,portal_access:["hr","employee","manager"],roles:["tenant_admin","hr_admin","hr_team","manager","employee"],account_status:"active"});
  }
  async function upsertBridge(authUserId, employeeId, tenantId){
    if(!authUserId || !employeeId || !tenantId) return;
    const existing=(data.links||[]).find(l=>l.user_id===authUserId && l.is_primary!==false);
    if(existing){
      await supabase.from("employee_user_links").update({employee_id:employeeId,tenant_id:tenantId,linked_at:new Date().toISOString()}).eq("id",existing.id);
    }else{
      await supabase.from("employee_user_links").insert({tenant_id:tenantId,employee_id:employeeId,user_id:authUserId,is_primary:true});
    }
  }
  async function saveRoles(authUserId, tenantId, roles){
    if(!authUserId || !tenantId) return;
    // Prefer RPC if installed by the schema revision; fallback to table writes.
    for(const role of roleOptions){
      const shouldEnable=(roles||[]).includes(role);
      const rpc=await supabase.rpc("rpc_set_user_role",{p_auth_user_id:authUserId,p_role:role,p_is_active:shouldEnable});
      if(rpc.error && /Could not find|does not exist|schema cache/i.test(rpc.error.message||"")) break;
    }
    await supabase.from("user_roles").update({is_active:false}).eq("user_id",authUserId);
    const roleRows=(roles||[]).map(role=>({tenant_id:tenantId,user_id:authUserId,role,is_active:true}));
    if(roleRows.length){
      const ins=await supabase.from("user_roles").insert(roleRows);
      if(ins.error && !/duplicate|violates/i.test(ins.error.message||"")) console.warn("Role insert warning",ins.error.message);
    }
  }
  async function saveUser(){
    if(!selected)return;
    setBusy(true); setMsg("Saving user access from frontend...");
    const employee=(data.allEmployees||data.employees||[]).find(e=>e.id===form.employee_id)||null;
    const tenantId=employee?.tenant_id || selected.tenant_id;
    try{
      // Preferred proper path: frontend calls backend RPC. This avoids manual SQL patching.
      if(selected.auth_user_id && employee?.employee_code){
        const rpc=await supabase.rpc("rpc_link_user_account_to_employee",{
          p_auth_user_id:selected.auth_user_id,
          p_employee_code:String(employee.employee_code),
          p_username:selected.username||selected.login_identifier||null,
          p_login_identifier:selected.login_identifier||selected.username||null,
          p_portal_access:form.portal_access||[],
          p_account_status:form.account_status||"active"
        });
        if(rpc.error && !/Could not find|does not exist|schema cache/i.test(rpc.error.message||"")) throw rpc.error;
      }
      // Fallback / direct app write for current schema.
      const patch={employee_id:form.employee_id||null, account_status:form.account_status||"active", portal_access:form.portal_access||[], updated_at:new Date().toISOString()};
      const {error:ue}=await supabase.from("user_accounts").update(patch).eq("id",selected.id);
      if(ue) throw ue;
      if(selected.auth_user_id && form.employee_id){
        await upsertBridge(selected.auth_user_id, form.employee_id, tenantId);
        await supabase.from("profiles").update({employee_id:form.employee_id}).eq("id",selected.auth_user_id);
      }
      if(selected.auth_user_id) await saveRoles(selected.auth_user_id, tenantId, form.roles||[]);
      setMsg("Saved through User Management. Sign out/in or refresh to reload portal context.");
      await reload();
    }catch(e){
      console.error(e); setMsg(e.message||String(e));
    }finally{setBusy(false);}
  }
  return <div className="grid user-mgmt-v2">
    <div className="card premium user-hero"><div><h2>User Management</h2><p>Frontend-managed identity linking. Link each login to an employee profile, then assign portal access and roles. No manual SQL should be needed for normal HR administration.</p></div><div className="grid4"><Stat label="Users" value={data.users.length}/><Stat label="Linked" value={data.users.filter(u=>u.linked_employee_id).length} tone="green"/><Stat label="Unlinked" value={data.users.filter(u=>!u.linked_employee_id).length} tone="amber"/><Stat label="Active Employees" value={data.employees.length}/></div></div>
    {unlinked.length>0&&<div className="card premium identity-warning"><h3>Unlinked logins need employee mapping</h3><p className="muted">These accounts have roles but no employee context. They will show “Employee profile not linked” until linked here.</p><div className="identity-chip-row">{unlinked.slice(0,8).map(u=><button className="badge amber clickable" key={u.id} onClick={()=>openUser(u)}>{u.username||u.login_identifier}</button>)}</div></div>}
    <div className="grid2 user-grid">
      <div className="card premium"><div className="toolbar"><input className="input" placeholder="Search user / employee / login..." value={q} onChange={e=>setQ(e.target.value)}/><button className="btn ghost" onClick={reload}>Refresh</button></div><div className="table"><table><thead><tr><th>User</th><th>Linked Employee</th><th>Status</th><th>Portal</th><th>Roles</th><th>Suggestion</th><th></th></tr></thead><tbody>{rows.map(u=>{const sug=suggestionsFor(u)[0]; return <tr key={u.id} className={selected?.id===u.id?"selectedRow":!u.linked_employee_id?"attentionRow":""}><td><b>{u.username||u.login_identifier}</b><div className="muted">{u.login_identifier||u.auth_user_id}</div></td><td>{u.employee?`${u.employee.employee_code} — ${u.employee.full_name}`:<span className="badge amber">Not linked</span>}</td><td><StatusBadge value={u.account_status}/></td><td>{(u.portal_access||[]).map(x=><span className="badge blue" key={x}>{x}</span>)}</td><td>{(u.roles||[]).map(x=><span className="badge green" key={x}>{x}</span>)}</td><td>{!u.linked_employee_id && sug?<span className="badge goldb">{sug.employee_code} · {sug.full_name}</span>:<span className="muted">—</span>}</td><td><button className="btn gold small" onClick={()=>openUser(u)}>Manage</button></td></tr>})}</tbody></table></div></div>
      <div className="card premium user-editor"><h3>Role / Profile Link</h3>{!selected?<p className="muted">Select a user to link employee and assign roles.</p>:<><p><b>{selected.username||selected.login_identifier}</b><br/><span className="muted">{selected.auth_user_id||"No auth user id"}</span></p>{/rohan/i.test(`${selected.username||''} ${selected.login_identifier||''} ${selected.display_name||''}`)&&<button className="btn ghost small" onClick={applyRohan}>Smart fill: Rohan Puri / 520 / HR Admin</button>}<label className="label">Linked Employee</label><select className="input" value={form.employee_id||""} onChange={e=>setForm({...form,employee_id:e.target.value})}><option value="">Not linked</option>{data.employees.map(e=><option key={e.id} value={e.id}>{e.employee_code} — {e.full_name} · {e.department_code||e.department_name||""}</option>)}</select><div className="smart-suggestions"><label className="label">Smart Suggestions</label>{suggestionsFor(selected).length?suggestionsFor(selected).map(e=><button key={e.id} className="badge clickable" onClick={()=>setForm({...form,employee_id:e.id})}>{e.employee_code} · {e.full_name}</button>):<span className="muted">No automatic match found</span>}</div><label className="label">Account Status</label><select className="input" value={form.account_status||"active"} onChange={e=>setForm({...form,account_status:e.target.value})}>{accountStatuses.map(x=><option key={x}>{x}</option>)}</select><label className="label">Portal Access</label><div className="checkGrid">{portalOptions.map(p=><label key={p}><input type="checkbox" checked={(form.portal_access||[]).includes(p)} onChange={()=>toggleArr("portal_access",p)}/> {p}</label>)}</div><label className="label">Roles</label><div className="checkGrid">{roleOptions.map(r=><label key={r}><input type="checkbox" checked={(form.roles||[]).includes(r)} onChange={()=>toggleArr("roles",r)}/> {r}</label>)}</div><div className="toolbar"><button className="btn gold" onClick={saveUser} disabled={busy}>{busy?"Saving...":"Save User Access"}</button></div>{msg&&<p className={msg.includes("Saved")?"notice good":"warning"}>{msg}</p>}<p className="muted small">This is the proper operational path. SQL should only be for one-time schema/RPC installation or diagnostics.</p></>}</div>
    </div>
  </div>
}

function fmtDate(d){const x=safeDateObj(d); if(!x)return d?String(d):"—"; return x.toLocaleDateString('en-GB');}
function StatusBadge({value}){const v=String(value||'').toLowerCase(); return <span className={cn('badge',v.includes('overdue')||v.includes('critical')?'critical':v.includes('due')||v.includes('pending')||v.includes('extended')?'amber':v.includes('confirmed')||v.includes('valid')||v.includes('ready')?'green':'muted')}>{value||'—'}</span>}

function LifecycleDashboard(){
  const [q,setQ]=useState('');
  const [type,setType]=useState('all');
  const [dept,setDept]=useState('all');
  const [sort,setSort]=useState({key:'event_date',dir:'asc'});
  const {data,loading,error,reload}=useLoad(async()=>{
    const [conf,backendEvents,gaps,queue,employeesRes]=await Promise.all([
      supabase.from('v_confirmation_tracker').select('*'),
      supabase.from('v_employee_events_calendar').select('*').order('event_date',{ascending:true}).limit(500),
      supabase.from('v_lifecycle_gaps').select('*'),
      supabase.from('v_notification_queue_dashboard').select('*').order('scheduled_for',{ascending:true}).limit(200),
      supabase.from('employees').select('id,employee_code,full_name,status,date_of_birth,date_of_joining,current_confirmation_due_date,standard_probation_end_date,probation_end_date,actual_confirmation_date,department_code,site_code,employee_type_code').order('employee_code').limit(7000)
    ]);
    for(const r of [conf,gaps,queue,employeesRes]) if(r.error) throw r.error;
    const activeEmployees=(employeesRes.data||[]).filter(e=>isActiveEmployeeStatus(e.status) && isOnRollEmployee(e));
    const activeById=Object.fromEntries(activeEmployees.map(e=>[e.id,e]));
    const activeByCode=Object.fromEntries(activeEmployees.map(e=>[String(e.employee_code||''),e]));
    const todayObj=new Date();
    const startIso=isoDateLocal(todayObj);
    const endIso=isoDateLocal(new Date(todayObj.getFullYear(), todayObj.getMonth(), todayObj.getDate()+365));
    function in365(iso){return iso && iso>=startIso && iso<=endIso;}
    const derived=[];
    activeEmployees.forEach(emp=>{
      const base={employee_id:emp.id,employee_code:emp.employee_code,full_name:emp.full_name,department_code:employeeDept(emp),site_code:emp.site_code||'',event_status:'upcoming',severity:'info',_source:'employee_master'};
      const dob=employeeDob(emp); const b=nextAnnualDate(dob,todayObj);
      if(in365(b)) derived.push({...base,event_id:`live-birthday-${emp.id}-${b}`,event_date:b,event_type:'birthday',event_title:`Birthday · ${emp.full_name}`,age_turning:ageOnDate(dob,b)});
      const doj=employeeDoj(emp); const a=nextAnnualDate(doj,todayObj);
      if(in365(a)) derived.push({...base,event_id:`live-anniversary-${emp.id}-${a}`,event_date:a,event_type:'work_anniversary',event_title:`Work Anniversary · ${emp.full_name}`,years_completed:yearsOnDate(doj,a)});
      const dueIso=isoDateLocal(safeDateObj(emp.current_confirmation_due_date||emp.standard_probation_end_date||emp.probation_end_date));
      if(dueIso && in365(dueIso) && !emp.actual_confirmation_date) derived.push({...base,event_id:`live-confirmation-${emp.id}-${dueIso}`,event_date:dueIso,event_type:'confirmation_due',event_title:`Confirmation Due · ${emp.full_name}`,severity:daysBetweenIso(startIso,dueIso)<=7?'attention':'info'});
    });
    const filteredBackend=(backendEvents.error?[]:(backendEvents.data||[])).filter(ev=>{
      const emp=activeById[ev.employee_id]||activeByCode[String(ev.employee_code||'')];
      return !!emp;
    }).map(ev=>{const emp=activeById[ev.employee_id]||activeByCode[String(ev.employee_code||'')]||{};return {...ev,employee_code:ev.employee_code||emp.employee_code,full_name:ev.full_name||ev.employee_name||emp.full_name,department_code:ev.department_code||employeeDept(emp),site_code:ev.site_code||emp.site_code||'',_source:'backend'};});
    const seen=new Set();
    const events=[];
    [...derived,...filteredBackend].forEach(e=>{
      const iso=isoDateLocal(safeDateObj(e.event_date)); if(!iso) return;
      const key=`${e.employee_id||e.employee_code}-${e.event_type}-${iso}`; if(seen.has(key)) return; seen.add(key);
      events.push({...e,event_date:iso,_urgency:eventUrgency({...e,event_date:iso},startIso)});
    });
    return {conf:(conf.data||[]).filter(x=>isActiveEmployeeStatus(x.employee_status||x.status||'active') && isOnRollEmployee(x)),events:events.sort((a,b)=>String(a.event_date).localeCompare(String(b.event_date))),gaps:gaps.data||[],queue:queue.data||[],employees:activeEmployees,viewWarning:backendEvents.error?.message||''};
  },[]);
  if(loading)return <Loading label="Lifecycle Dashboard"/>;
  if(error)return <div className="card premium"><p className="error">{error.message||String(error)}</p><p className="muted">Lifecycle now uses Employee Master fallback, but core views may still require backend SQL.</p></div>;
  const depts=[...new Set((data.events||[]).map(e=>e.department_code||'Unmapped'))].sort();
  const types=[...new Set((data.events||[]).map(e=>e.event_type||'event'))].sort();
  const events=(data.events||[]).filter(e=>(type==='all'||e.event_type===type)&&(dept==='all'||(e.department_code||'Unmapped')===dept)&&(!q||`${e.employee_code} ${e.full_name} ${e.event_type} ${e.department_code}`.toLowerCase().includes(q.toLowerCase()))).sort((a,b)=>cmpSort(a,b,sort.key,sort.dir));
  const overdue=data.conf.filter(x=>x.tracker_bucket==='overdue').length;
  const due30=data.conf.filter(x=>x.tracker_bucket==='due_30'||x.tracker_bucket==='due').length;
  const gapRows=data.gaps.filter(x=>(x.gaps||[]).length).length;
  const draftMail=data.queue.filter(x=>x.status==='draft').length;
  const missingDob=(data.employees||[]).filter(e=>!safeDateObj(employeeDob(e))).length;
  function toggleSort(key){setSort(s=>({key,dir:s.key===key&&s.dir==='asc'?'desc':'asc'}));}
  async function refreshAll(){try{await supabase.rpc('rpc_recalculate_employee_lifecycle_dates'); await supabase.rpc('rpc_generate_employee_events',{}); await supabase.rpc('rpc_generate_notification_queue',{}); reload();}catch(e){console.error(e.message||String(e)); reload();}}
  return <div className="grid lifecycle-v2">
    <div className="toolbar wrap"><button className="btn gold" onClick={refreshAll}>Recalculate Dates + Generate Events/Reminders</button><button className="btn ghost" onClick={reload}>Refresh</button><select className="input" value={type} onChange={e=>setType(e.target.value)}><option value="all">All event types</option>{types.map(t=><option key={t} value={t}>{eventTypeNice(t)}</option>)}</select><select className="input" value={dept} onChange={e=>setDept(e.target.value)}><option value="all">All departments</option>{depts.map(d=><option key={d} value={d}>{d}</option>)}</select><input className="input" placeholder="Search lifecycle events..." value={q} onChange={e=>setQ(e.target.value)}/></div>
    <div className="grid4"><Stat label="Confirmations Overdue" value={overdue} icon="!" tone="critical"/><Stat label="Due in 30 Days" value={due30} icon="☑" tone="amber"/><Stat label="Missing DOB" value={missingDob} icon="△" tone={missingDob?'amber':'green'}/><Stat label="Dormant Mail Drafts" value={draftMail} icon="✉" tone="muted"/></div>
    {data.viewWarning&&<div className="warning">Backend event view warning: {data.viewWarning}. Showing Employee Master derived events instead.</div>}
    <div className="card premium"><div className="cardTitle"><h3>Upcoming Lifecycle Events</h3><span className="badge">{events.length}</span></div><div className="table"><table><thead><tr><th onClick={()=>toggleSort('event_date')}>Date</th><th onClick={()=>toggleSort('event_type')}>Event</th><th onClick={()=>toggleSort('full_name')}>Employee</th><th onClick={()=>toggleSort('department_code')}>Department</th><th>Status</th><th>Source</th></tr></thead><tbody>{events.slice(0,80).map(e=><tr key={e.event_id}><td>{fmtDate(e.event_date)}</td><td>{eventTypeNice(e.event_type)}</td><td><b>{e.employee_code}</b> · {e.full_name}</td><td>{e.department_code||'—'}</td><td><StatusBadge value={e._urgency||e.event_status}/></td><td><span className="badge muted">{e._source}</span></td></tr>)}</tbody></table></div></div>
    <div className="card premium"><h3>Lifecycle Gaps</h3><div className="table"><table><thead><tr><th>Employee</th><th>Gaps</th></tr></thead><tbody>{data.gaps.filter(x=>(x.gaps||[]).length).slice(0,12).map(g=><tr key={g.employee_id}><td>{g.employee_code} · {g.full_name}</td><td>{(g.gaps||[]).map(x=><span className="badge amber" key={x}>{x}</span>)}</td></tr>)}</tbody></table></div></div>
  </div>
}

function ConfirmationTracker(){
  const {data,loading,error,reload}=useLoad(async()=>{
    const r=await supabase.from('v_confirmation_tracker').select('*').order('current_confirmation_due_date',{ascending:true});
    if(r.error)throw r.error;
    const rows=r.data||[];
    const ids=[...new Set(rows.map(x=>x.employee_id).filter(Boolean))];
    let empMap={};
    if(ids.length){
      const e=await supabase.from('employees').select('*').in('id',ids);
      if(!e.error) empMap=Object.fromEntries((e.data||[]).map(x=>[x.id,x]));
    }
    return rows.map(x=>{
      const emp=empMap[x.employee_id]||{};
      const merged={...x,...Object.fromEntries(Object.entries(emp).filter(([k,v])=>x[k]===undefined || x[k]===null || x[k]===''))};
      return {
        ...merged,
        _employee_status: emp.status||x.status||x.employee_status||x.employment_status||'active',
        _department: employeeDept({...emp,...x}),
        _confirm_status: employeeConfirmStatus({...emp,...x}),
        _due_iso: isoDateLocal(safeDateObj(x.current_confirmation_due_date||emp.current_confirmation_due_date||x.standard_probation_end_date||emp.standard_probation_end_date)),
        _actual_iso: isoDateLocal(safeDateObj(x.actual_confirmation_date||emp.actual_confirmation_date))
      };
    }).filter(x=>isActiveEmployeeStatus(x._employee_status) && isOnRollEmployee(x));
  },[]);
  const [q,setQ]=useState('');
  const [bucket,setBucket]=useState('all');
  const [view,setView]=useState('tracker');
  const [dept,setDept]=useState('all');
  const [sort,setSort]=useState({key:'current_confirmation_due_date',dir:'asc'});
  const [modal,setModal]=useState(null);
  const [form,setForm]=useState({});
  const [busy,setBusy]=useState(false);
  const [msg,setMsg]=useState('');
  if(loading)return <Loading label="Confirmations"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p><p className="muted">Run SQL 25 first to create the v_confirmation_tracker view.</p></div>;
  const nowIso=isoDateLocal(new Date());
  function rowBucket(r){
    const raw=String(r.tracker_bucket||r.confirmation_status||r._confirm_status||'').toLowerCase();
    if(raw.includes('confirm') && !raw.includes('unconfirm')) return 'confirmed';
    if(raw.includes('pending')) return 'pending';
    const due=r._due_iso||isoDateLocal(safeDateObj(r.current_confirmation_due_date));
    const d=due?daysBetweenIso(nowIso,due):null;
    if(d!==null && d<0 && !r._actual_iso) return 'overdue';
    if(d!==null && d<=30 && !r._actual_iso) return 'due';
    if(r._actual_iso) return 'confirmed';
    return raw.includes('not_applicable')?'not_applicable':'open';
  }
  const normalized=data.map(r=>({...r,_bucket:rowBucket(r)}));
  const depts=[...new Set(normalized.map(r=>r._department||'Unmapped'))].sort();
  const baseFiltered=normalized.filter(r=>dept==='all'||r._department===dept);
  const buckets=[
    {id:'all',label:'All',count:baseFiltered.length},
    {id:'overdue',label:'Overdue',count:baseFiltered.filter(r=>r._bucket==='overdue').length},
    {id:'due',label:'Due Soon',count:baseFiltered.filter(r=>r._bucket==='due').length},
    {id:'pending',label:'Pending Review',count:baseFiltered.filter(r=>r._bucket==='pending').length},
    {id:'confirmed',label:'Confirmed',count:baseFiltered.filter(r=>r._bucket==='confirmed').length},
    {id:'not_applicable',label:'Not Applicable',count:baseFiltered.filter(r=>r._bucket==='not_applicable').length},
  ];
  const filtered=baseFiltered.filter(r=>{
    const qs=q.toLowerCase();
    const matchQ=!q||`${r.full_name||''} ${r.employee_code||''} ${r.reporting_manager_name||''} ${r._department||''}`.toLowerCase().includes(qs);
    const matchB=bucket==='all'||r._bucket===bucket;
    return matchQ && matchB;
  }).sort((a,b)=>cmpSort(a,b,sort.key,sort.dir));
  function toggleSort(key){setSort(s=>({key,dir:s.key===key&&s.dir==='asc'?'desc':'asc'}));}
  function openModal(type,row){setMsg('');setModal({type,row}); if(type==='open')setForm({due_date:row.current_confirmation_due_date||row._due_iso||'',note:'Confirmation cycle opened'}); if(type==='extend')setForm({extend_to:'',reason:'Performance review extension'}); if(type==='confirm')setForm({date:new Date().toISOString().slice(0,10),remarks:'Confirmed on review'});}
  async function submitModal(){if(busy)return; setBusy(true); const {type,row}=modal; let err=null; if(type==='open'){const r=await supabase.rpc('rpc_create_confirmation_cycle',{p_employee_id:row.employee_id,p_due_date:form.due_date||null,p_note:form.note||null}); err=r.error;} else if(type==='extend'){if(!row.current_cycle_id){setBusy(false);setMsg('No current cycle found. Open cycle first.');return;} const r=await supabase.rpc('rpc_extend_confirmation',{p_confirmation_cycle_id:row.current_cycle_id,p_extension_to:form.extend_to,p_reason:form.reason||''}); err=r.error;} else if(type==='confirm'){if(!row.current_cycle_id){setBusy(false);setMsg('No current cycle found. Open cycle first.');return;} const r=await supabase.rpc('rpc_confirm_employee',{p_confirmation_cycle_id:row.current_cycle_id,p_actual_confirmation_date:form.date,p_remarks:form.remarks||''}); err=r.error;} setBusy(false); if(err){setMsg(err.message||String(err));return;} setModal(null); reload();}
  const deptStats=depts.map(d=>{const rows=normalized.filter(r=>r._department===d); return {department:d,total:rows.length,overdue:rows.filter(r=>r._bucket==='overdue').length,due:rows.filter(r=>r._bucket==='due').length,pending:rows.filter(r=>r._bucket==='pending').length,confirmed:rows.filter(r=>r._bucket==='confirmed').length};}).sort((a,b)=>(b.overdue+b.due+b.pending)-(a.overdue+a.due+a.pending));
  const maxDept=Math.max(1,...deptStats.map(x=>x.total));
  const overdue=buckets.find(b=>b.id==='overdue')?.count||0, due30=buckets.find(b=>b.id==='due')?.count||0, confirmed=buckets.find(b=>b.id==='confirmed')?.count||0, pending=buckets.find(b=>b.id==='pending')?.count||0;
  const completionPct=Math.round((confirmed/Math.max(1,baseFiltered.length))*100);
  return <div className="grid confirmations-v3"><div className="grid4"><Stat label="Overdue" value={overdue} icon="⚠" tone="redb"/><Stat label="Due Soon" value={due30} icon="◷" tone="amber"/><Stat label="Pending Review" value={pending} icon="☑" tone="goldb"/><Stat label="Confirmed" value={confirmed} icon="✓" tone="green"/></div>
    <div className="card premium confirm-intel"><div className="cardTitle"><h3>Confirmation Intelligence</h3><button className="btn ghost small" onClick={reload}>Refresh</button></div><div className="toolbar wrap"><select className="input" value={view} onChange={e=>setView(e.target.value)}><option value="tracker">Tracker view</option><option value="department">Department graph</option><option value="racetrack">Racetrack view</option><option value="confirmed">Confirmed people</option></select><select className="input" value={dept} onChange={e=>setDept(e.target.value)}><option value="all">All departments</option>{depts.map(d=><option key={d} value={d}>{d}</option>)}</select><input className="input" style={{maxWidth:360}} placeholder="Search employee, code, manager, department…" value={q} onChange={e=>setQ(e.target.value)}/><span className="badge green">{completionPct}% confirmed</span><span className="muted">{filtered.length} of {baseFiltered.length} active on-roll employees</span></div><div className="tabs" style={{marginBottom:14}}>{buckets.map(b=><button key={b.id} className={cn(bucket===b.id&&"active")} onClick={()=>setBucket(b.id)}>{b.label} <span className={cn("badge",b.id==='overdue'?'redb':b.id==='due'?'amber':b.id==='pending'?'goldb':b.id==='confirmed'?'green':'muted')} style={{marginLeft:4}}>{b.count}</span></button>)}</div>{msg&&<p className="warning">{msg}</p>}
      {view==='department'&&<div className="dept-bars">{deptStats.map(d=><button key={d.department} className="dept-bar" onClick={()=>{setDept(d.department);setView('tracker')}}><b>{d.department}</b><span>{d.total} people · {d.overdue} overdue · {d.due} due · {d.confirmed} confirmed</span><i style={{width:`${Math.max(4,(d.total/maxDept)*100)}%`}}/><em style={{width:`${Math.max(0,((d.overdue+d.due+d.pending)/Math.max(1,d.total))*100)}%`}}/></button>)}</div>}
      {view==='racetrack'&&<div className="race-track">{deptStats.map((d,i)=><div key={d.department} className="race-row"><strong>{i+1}</strong><b>{d.department}</b><div><span style={{width:`${Math.max(4,(d.confirmed/Math.max(1,d.total))*100)}%`}}>{Math.round((d.confirmed/Math.max(1,d.total))*100)}%</span></div><em>{d.confirmed}/{d.total} confirmed</em></div>)}</div>}
      {(view==='tracker'||view==='confirmed')&&<div className="table"><table><thead><tr><th onClick={()=>toggleSort('full_name')}>Employee</th><th onClick={()=>toggleSort('date_of_joining')}>DOJ</th><th onClick={()=>toggleSort('standard_probation_end_date')}>Standard End</th><th onClick={()=>toggleSort('current_confirmation_due_date')}>Current Due</th><th onClick={()=>toggleSort('actual_confirmation_date')}>Actual</th><th onClick={()=>toggleSort('tracker_bucket')}>Status</th><th onClick={()=>toggleSort('reporting_manager_name')}>Manager</th><th style={{minWidth:180}}>Actions</th></tr></thead><tbody>{(view==='confirmed'?filtered.filter(r=>r._bucket==='confirmed'):filtered).map(r=><tr key={r.employee_id}><td><div style={{fontWeight:700,color:'var(--t0)'}}>{r.full_name}</div><div className="muted">{r.employee_code} · {r._department||''}</div></td><td>{fmtDate(r.date_of_joining)}</td><td>{fmtDate(r.standard_probation_end_date)}</td><td style={{fontWeight:600}}>{fmtDate(r.current_confirmation_due_date)}</td><td>{fmtDate(r.actual_confirmation_date)}</td><td><StatusBadge value={r._bucket}/><div className="muted" style={{marginTop:3,fontSize:11}}>{r.confirmation_status}</div></td><td>{r.reporting_manager_name||<span className="muted">—</span>}</td><td><div style={{display:'flex',gap:4,flexWrap:'wrap'}}><button className="btn ghost small" onClick={()=>openModal('open',r)}>Open Cycle</button><button className="btn ghost small" onClick={()=>openModal('extend',r)} disabled={!r.current_cycle_id}>Extend</button><button className="btn gold small" onClick={()=>openModal('confirm',r)} disabled={!r.current_cycle_id}>Confirm</button></div></td></tr>)}</tbody></table></div>}
    </div>{modal&&<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,.45)',zIndex:9000,display:'flex',alignItems:'center',justifyContent:'center'}} onClick={e=>{if(e.target===e.currentTarget)setModal(null)}}><div className="card" style={{width:460,borderRadius:20,padding:28,boxShadow:'var(--sh3)'}}><div className="cardTitle"><h3>{modal.type==='open'?'Open Confirmation Cycle':modal.type==='extend'?'Extend Probation':'Confirm Employee'}</h3><button className="btn ghost small" onClick={()=>setModal(null)}>✕</button></div><div className="muted" style={{marginBottom:16}}>{modal.row.full_name} · {modal.row.employee_code}</div>{modal.type==='open'&&<><label className="label">Due / Probation End Date</label><input className="input" type="date" value={form.due_date} onChange={e=>setForm({...form,due_date:e.target.value})}/><label className="label">Opening Note</label><input className="input" value={form.note} onChange={e=>setForm({...form,note:e.target.value})}/></>}{modal.type==='extend'&&<><label className="label">Extend Probation To</label><input className="input" type="date" value={form.extend_to} onChange={e=>setForm({...form,extend_to:e.target.value})}/><label className="label">Extension Reason</label><input className="input" value={form.reason} onChange={e=>setForm({...form,reason:e.target.value})}/></>}{modal.type==='confirm'&&<><label className="label">Actual Confirmation Date</label><input className="input" type="date" value={form.date} onChange={e=>setForm({...form,date:e.target.value})}/><label className="label">Remarks</label><input className="input" value={form.remarks} onChange={e=>setForm({...form,remarks:e.target.value})}/></>}<div className="toolbar" style={{marginTop:20,justifyContent:'flex-end'}}><button className="btn ghost" onClick={()=>setModal(null)}>Cancel</button><button className="btn gold" onClick={submitModal} disabled={busy}>{busy?'Saving…':'Save'}</button></div></div></div>}</div>
}


function EventsCalendar(){
  const [days,setDays]=useState(365);
  const [type,setType]=useState('all');
  const [dept,setDept]=useState('all');
  const [source,setSource]=useState('all');
  const [urgency,setUrgency]=useState('all');
  const [mode,setMode]=useState('smart');
  const [q,setQ]=useState('');
  const [zoom,setZoom]=useState(1);
  const [sort,setSort]=useState({key:'event_date',dir:'asc'});
  const {data,loading,error,reload}=useLoad(async()=>{
    const [viewRes,empRes]=await Promise.all([
      supabase.from('v_employee_events_calendar').select('*').order('event_date',{ascending:true}).limit(2000),
      supabase.from('employees').select('*').order('employee_code').limit(7000)
    ]);
    if(empRes.error) throw empRes.error;
    const employees=(empRes.data||[]).filter(e=>isActiveEmployeeStatus(e.status) && isOnRollEmployee(e));
    const employeeById=Object.fromEntries(employees.map(e=>[e.id,e]));
    const employeeByCode=Object.fromEntries(employees.map(e=>[String(e.employee_code||''),e]));
    const dbEvents=(viewRes.error?[]:(viewRes.data||[])).filter(e=>{
      const emp=employeeById[e.employee_id] || employeeByCode[String(e.employee_code||'')];
      return !!emp || (!e.employee_id && isActiveEmployeeStatus(e.employee_status||e.status));
    }).map(e=>{
      const emp=employeeById[e.employee_id] || employeeByCode[String(e.employee_code||'')] || {};
      return {...e,_source:'backend',employee_code:e.employee_code||emp.employee_code,full_name:e.full_name||e.employee_name||emp.full_name,department_code:e.department_code||emp.department_code||emp.department_name||emp.function||'',site_code:e.site_code||emp.site_code||emp.site_name||''};
    });
    return {employees,dbEvents,viewError:viewRes.error?.message||''};
  },[]);
  if(loading)return <Loading label="Events Calendar"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  const now=new Date();
  const startIso=isoDateLocal(now);
  const end=new Date(now.getFullYear(),now.getMonth(),now.getDate()+Number(days||365));
  const endIso=isoDateLocal(end);
  function withinWindow(iso){return iso && iso>=startIso && iso<=endIso;}
  function makeDerivedEvents(){
    const out=[];
    (data.employees||[]).forEach(emp=>{
      const deptName=employeeDept(emp);
      const base={employee_id:emp.id,employee_code:emp.employee_code,full_name:emp.full_name,department_code:deptName,site_code:emp.site_code||emp.site_name||'',severity:'info',event_status:'upcoming',_source:'employee_master'};
      const rawDob=employeeDob(emp);
      const bday=nextAnnualDate(rawDob,now);
      if(withinWindow(bday)) out.push({...base,event_id:`birthday-${emp.id||emp.employee_code}-${bday}`,event_date:bday,event_type:'birthday',event_title:`Birthday · ${emp.full_name}`,age_turning:ageOnDate(rawDob,bday)});
      const rawDoj=employeeDoj(emp);
      const ann=nextAnnualDate(rawDoj,now);
      if(withinWindow(ann)) out.push({...base,event_id:`anniversary-${emp.id||emp.employee_code}-${ann}`,event_date:ann,event_type:'work_anniversary',event_title:`Work Anniversary · ${emp.full_name}`,years_completed:yearsOnDate(rawDoj,ann)});
      const due=emp.current_confirmation_due_date||emp.standard_probation_end_date||emp.probation_end_date;
      const dueIso=isoDateLocal(safeDateObj(due));
      if(dueIso && withinWindow(dueIso) && !emp.actual_confirmation_date){
        out.push({...base,event_id:`confirmation-${emp.id||emp.employee_code}-${dueIso}`,event_date:dueIso,event_type:'confirmation_due',event_title:`Confirmation Due · ${emp.full_name}`,severity:daysBetweenIso(startIso,dueIso)<=7?'attention':'info'});
      }
    });
    return out;
  }
  const merged=[]; const seen=new Set();
  [...makeDerivedEvents(),...(data.dbEvents||[])].forEach(e=>{
    const iso=isoDateLocal(safeDateObj(e.event_date));
    if(!iso) return;
    const keyCode=e.employee_id||e.employee_code||e.full_name;
    const k=`${keyCode}-${e.event_type}-${iso}`;
    if(seen.has(k)) return;
    seen.add(k);
    const urg=eventUrgency({...e,event_date:iso},startIso);
    merged.push({...e,event_date:iso,event_status:e.event_status||urg,_urgency:urg,_month:monthNameFromIso(iso)});
  });
  const employeesMissingDob=(data.employees||[]).filter(e=>!safeDateObj(employeeDob(e))).length;
  const employeesMissingDoj=(data.employees||[]).filter(e=>!safeDateObj(employeeDoj(e))).length;
  const rohan=(data.employees||[]).find(e=>String(e.employee_code)==='520'||/rohan puri/i.test(String(e.full_name||'')));
  const rohanDob=rohan?employeeDob(rohan):'';
  const rohanBday=rohan?nextAnnualDate(rohanDob,now):'';
  const departments=[...new Set(merged.map(e=>e.department_code||'Unmapped'))].sort();
  const types=[...new Set(merged.map(e=>e.event_type||'event'))].sort();
  const filtered=merged.filter(e=>{
    const matchWindow=withinWindow(e.event_date);
    const matchType=type==='all'||e.event_type===type;
    const matchDept=dept==='all'||(e.department_code||'Unmapped')===dept;
    const matchSource=source==='all'||e._source===source;
    const matchUrgency=urgency==='all'||e._urgency===urgency;
    const matchQ=!q||`${e.employee_code} ${e.full_name} ${e.event_type} ${e.department_code} ${e.event_title}`.toLowerCase().includes(q.toLowerCase());
    return matchWindow && matchType && matchDept && matchSource && matchUrgency && matchQ;
  }).sort((a,b)=>cmpSort(a,b,sort.key,sort.dir));
  function toggleSort(key){setSort(s=>({key,dir:s.key===key&&s.dir==='asc'?'desc':'asc'}));}
  async function generate(){const {error}=await supabase.rpc('rpc_generate_employee_events',{}); if(error)console.error(error.message); reload();}
  const groups=filtered.reduce((a,e)=>{const k=mode==='month'?e._month:(e.event_date||'No date');(a[k]=a[k]||[]).push(e);return a},{});
  const todayEvents=merged.filter(e=>e._urgency==='today').length;
  const weekEvents=merged.filter(e=>e._urgency==='this week').length;
  const birthdayCount=filtered.filter(e=>e.event_type==='birthday').length;
  const anniversaryCount=filtered.filter(e=>e.event_type==='work_anniversary').length;
  const confirmationCount=filtered.filter(e=>e.event_type==='confirmation_due').length;
  const nextEvent=filtered[0];
  return <div className="grid events-calendar-v2 smart-events">
    <div className="card premium event-hero"><div><h2>Events Intelligence</h2><p>Active on-roll employees only. Birthdays, anniversaries and confirmation dues are calculated live from Employee Master, with backend events de-duplicated.</p>{data.viewError&&<p className="warning">Backend event view warning: {data.viewError}</p>}{rohan&&<p className={rohanBday?"notice good":"warning"}>Rohan check: DB DOB = {rohanDob?fmtDate(rohanDob):'blank in employees table'} · Next birthday = {rohanBday?fmtDate(rohanBday):'not readable'}</p>}</div><div className="grid4"><Stat label="Visible Events" value={filtered.length}/><Stat label="Today" value={todayEvents} tone="green"/><Stat label="Next 7 Days" value={weekEvents} tone="goldb"/><Stat label="Birthdays" value={birthdayCount} tone="blue"/></div></div>
    <div className="event-insights"><div className="smart-tile"><span>Next event</span><b>{nextEvent?fmtDate(nextEvent.event_date):'—'}</b><em>{nextEvent?`${eventTypeNice(nextEvent.event_type)} · ${nextEvent.full_name}`:'No event in current filter'}</em></div><div className="smart-tile"><span>Anniversaries</span><b>{anniversaryCount}</b><em>Work anniversary events in current filter</em></div><div className="smart-tile"><span>Confirmations</span><b>{confirmationCount}</b><em>Due dates included from employee master</em></div><div className={cn('smart-tile',employeesMissingDob?'warn':'')}><span>Data quality</span><b>{employeesMissingDob}</b><em>active on-roll employees missing DOB · {employeesMissingDoj} missing DOJ</em></div></div>
    <div className="card event-controls"><div className="toolbar wrap"><button className="btn gold" onClick={generate}>Generate Backend Events</button><button className="btn ghost" onClick={reload}>Refresh</button><button className="btn ghost" onClick={()=>downloadCsv('employee_events_calendar.csv',filtered)}>Download CSV</button><button className="btn ghost" onClick={()=>{setType('all');setDept('all');setSource('all');setUrgency('all');setQ('');setDays(365);}}>Reset Filters</button><select className="input" value={days} onChange={e=>setDays(Number(e.target.value))}>{[30,60,90,180,365].map(d=><option key={d} value={d}>Next {d} days</option>)}</select><select className="input" value={type} onChange={e=>setType(e.target.value)}><option value="all">All event types</option>{types.map(t=><option key={t} value={t}>{eventTypeNice(t)}</option>)}</select><select className="input" value={dept} onChange={e=>setDept(e.target.value)}><option value="all">All departments</option>{departments.map(d=><option key={d} value={d}>{d}</option>)}</select><select className="input" value={urgency} onChange={e=>setUrgency(e.target.value)}><option value="all">All urgency</option>{['today','this week','this month','upcoming'].map(u=><option key={u} value={u}>{humanLabel(u)}</option>)}</select><select className="input" value={source} onChange={e=>setSource(e.target.value)}><option value="all">All sources</option><option value="employee_master">Employee master derived</option><option value="backend">Backend generated</option></select><select className="input" value={mode} onChange={e=>setMode(e.target.value)}>{['smart','list','cards','month'].map(x=><option key={x} value={x}>{humanLabel(x)} view</option>)}</select><input className="input" value={q} onChange={e=>setQ(e.target.value)} placeholder="Search employee, type, dept..."/><button className="btn ghost small" onClick={()=>setZoom(z=>Math.max(.75,Number((z-.1).toFixed(2))))}>−</button><span className="badge">{Math.round(zoom*100)}%</span><button className="btn ghost small" onClick={()=>setZoom(z=>Math.min(1.5,Number((z+.1).toFixed(2))))}>+</button></div></div>
    <div className="event-body" style={{fontSize:`${zoom}em`}}>{(mode==='smart'||mode==='list')&&Object.entries(groups).map(([m,rows])=><div className="card premium event-day-card" key={m}><div className="cardTitle"><h3>{m}</h3><span className="badge">{rows.length}</span></div><div className="table"><table><thead><tr><th onClick={()=>toggleSort('event_date')}>Date</th><th onClick={()=>toggleSort('event_type')}>Type</th><th onClick={()=>toggleSort('full_name')}>Employee</th><th>Detail</th><th onClick={()=>toggleSort('department_code')}>Department</th><th>Urgency</th><th>Source</th></tr></thead><tbody>{rows.map(e=><tr key={e.event_id} className={cn(e._urgency==='today'&&'todayRow')}><td>{fmtDate(e.event_date)}</td><td><span className={cn('event-type-pill',e.event_type)}>{eventTypeNice(e.event_type)}</span></td><td><b>{e.employee_code}</b> · {e.full_name}</td><td>{e.event_type==='birthday'&&e.age_turning?`Turning ${e.age_turning}`:e.event_type==='work_anniversary'&&e.years_completed?`${e.years_completed} years`:e.event_title||'—'}</td><td>{e.department_code||'—'}</td><td><span className={cn('badge',e._urgency==='today'?'green':e._urgency==='this week'?'amber':'muted')}>{e._urgency}</span></td><td><span className="badge muted">{e._source}</span></td></tr>)}</tbody></table></div></div>)}{mode==='cards'&&<div className="event-card-grid">{filtered.map(e=><div className={cn('event-card',e.event_type,e._urgency==='today'&&'today')} key={e.event_id}><span>{fmtDate(e.event_date)}</span><b>{eventTypeNice(e.event_type)}</b><p>{e.employee_code} · {e.full_name}</p><em>{e.department_code||'—'} · {e.site_code||'—'}</em><small>{e.event_type==='birthday'&&e.age_turning?`Turning ${e.age_turning}`:e.event_type==='work_anniversary'&&e.years_completed?`${e.years_completed} years`:e._urgency}</small></div>)}</div>}{mode==='month'&&Object.entries(groups).map(([m,rows])=><div className="card premium month-board" key={m}><h3>{m}</h3><div className="month-list">{rows.map(e=><button key={e.event_id}><b>{String(e.event_date).slice(8,10)}</b><span>{eventTypeNice(e.event_type)}</span><em>{e.full_name}</em></button>)}</div></div>)}</div>
  </div>
}

function ReminderDashboard(){
  const {data,loading,error,reload}=useLoad(async()=>{const [q,r,t]=await Promise.all([supabase.from('v_notification_queue_dashboard').select('*').order('scheduled_for',{ascending:true}).limit(500),supabase.from('notification_rules_v2').select('*').order('rule_code'),supabase.from('notification_templates_v2').select('*').order('template_code')]); for(const x of [q,r,t]) if(x.error) throw x.error; return {queue:q.data||[],rules:r.data||[],templates:t.data||[]}},[]);
  if(loading)return <div className="empty">Loading reminders...</div>; if(error)return <div className="card premium"><p className="error">{error}</p><p className="muted">Run SQL 25 first.</p></div>;
  async function generate(){const {error}=await supabase.rpc('rpc_generate_notification_queue',{}); if(error)console.error(error.message); else reload();}
  return <div className="grid"><div className="card premium"><div className="cardTitle"><h3>Reminder Queue</h3><span className="badge amber">Mail dormant</span></div><p className="muted">Emails are prepared as drafts/ready items only. Actual sending is disabled until a backend mail sender is configured.</p><div className="toolbar"><button className="btn gold" onClick={generate}>Generate Reminder Queue</button><button className="btn ghost" onClick={()=>downloadCsv('notification_queue.csv',data.queue)}>Download Queue</button></div></div><div className="table card"><table><thead><tr><th>When</th><th>Channel</th><th>Status</th><th>Employee</th><th>Recipient</th><th>Subject</th><th>Mail Enabled?</th></tr></thead><tbody>{data.queue.map(n=><tr key={n.notification_id}><td>{fmtDate(n.scheduled_for)}</td><td>{n.channel}</td><td><StatusBadge value={n.status}/></td><td>{n.employee_name}</td><td>{n.recipient_employee_name||n.recipient_email||n.recipient_role}</td><td>{n.subject_rendered}</td><td>{String(n.is_mail_enabled)}</td></tr>)}</tbody></table></div><div className="card premium"><h3>Rules</h3><div className="table"><table><thead><tr><th>Rule</th><th>Event</th><th>Recipient</th><th>Channel</th><th>Mail</th></tr></thead><tbody>{data.rules.map(r=><tr key={r.id}><td>{r.rule_code}<br/><span>{r.rule_name}</span></td><td>{r.event_type}</td><td>{r.recipient_role}</td><td>{r.channel}</td><td><StatusBadge value={r.is_mail_enabled?'enabled':'dormant'}/></td></tr>)}</tbody></table></div></div></div>
}

function ManagerWorkbench(){
  const [tab,setTab]=useState("actions");
  const [roleTitle,setRoleTitle]=useState("");
  const [mrfRef,setMrfRef]=useState("");
  const [assessment,setAssessment]=useState("");
  const [questionSet,setQuestionSet]=useState([]);
  const {data,loading,error,reload}=useLoad(async()=>{
    const safe=async(label,query)=>{
      const r=await query;
      return {label,data:r.data||[],error:r.error?r.error.message:null};
    };
    const [actions,team,summary,interviews]=await Promise.all([
      safe("actions",supabase.from("v_manager_pending_actions").select("*").order("action_date",{ascending:true}).limit(500)),
      safe("team",supabase.from("v_manager_my_team").select("*").order("employee_code").limit(1000)),
      safe("summary",supabase.from("v_manager_workbench_summary").select("*").limit(1)),
      safe("interviews",supabase.from("v_manager_interview_tasks").select("*").order("scheduled_at",{ascending:true}).limit(200))
    ]);
    return {actions:actions.data,team:team.data,summary:(summary.data||[])[0]||{},interviews:interviews.data,warnings:[actions,team,summary,interviews].filter(x=>x.error).map(x=>`${x.label}: ${x.error}`)};
  },[]);
  if(loading)return <Loading label="Manager Workbench"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p></div>;

  const actions=data.actions||[];
  const team=data.team||[];
  const interviews=data.interviews||[];
  const pending=actions.filter(a=>!["approved","rejected","completed","closed"].includes(String(a.status||"").toLowerCase()));
  const direct=team.filter(t=>String(t.relationship_type||"primary_manager").includes("primary") || t.is_direct_report);
  const dotted=team.filter(t=>String(t.relationship_type||"").includes("dotted") || t.is_dotted_report);

  function generateInterviewQuestions(){
    const role=roleTitle||"this role";
    const ref=mrfRef?`MRF ${mrfRef}`:"MRF not entered";
    setQuestionSet([
      `Role context (${ref}): In your understanding, what are the 3 most important outcomes for ${role} in the first 6 months?`,
      `Functional depth: Walk us through one complex problem you solved that is closest to ${role}. What data did you use and what was the result?`,
      "Thinking: Tell us about a time you had incomplete information. How did you decide, and what did you later learn?",
      "Execution: Give an example where you had to close work despite delays from other people or functions.",
      "Ownership: Describe a situation where something was not formally your responsibility but you still took charge.",
      "Quality mindset: What checks do you put in place to avoid avoidable mistakes?",
      "Collaboration: Tell us about a time you had a disagreement with a senior/stakeholder. How did you handle it?",
      "Role fit: What will make you stay and grow in this role, and what kind of environment will not work for you?"
    ]);
  }
  function copyInterviewPack(){
    const txt=[
      `Interview Pack`,
      `MRF Reference: ${mrfRef||"—"}`,
      `Role: ${roleTitle||"—"}`,
      "",
      "Suggested Questions:",
      ...questionSet.map((q,i)=>`${i+1}. ${q}`),
      "",
      "Assessment Notes:",
      assessment||""
    ].join("\n");
    copyTextToClipboard(txt);
  }

  return <div className="manager-workbench grid">
    <div className="card premium manager-hero">
      <div>
        <h2>Manager Workbench</h2>
        <p className="muted">One place for team, org visibility, approvals, confirmations and interview tasks.</p>
        {data.warnings?.length>0&&<p className="warning">Some manager views are not installed yet: {data.warnings.join(" | ")}</p>}
      </div>
      <div className="manager-kpis">
        <div><b>{team.length}</b><span>Team</span></div>
        <div><b>{direct.length}</b><span>Direct</span></div>
        <div><b>{dotted.length}</b><span>Dotted</span></div>
        <div><b>{pending.length}</b><span>Pending</span></div>
      </div>
    </div>

    <div className="tabs manager-tabs">
      {["actions","team","interviews","assessment"].map(t=><button key={t} className={cn(tab===t&&"active")} onClick={()=>setTab(t)}>{humanLabel(t)}</button>)}
      <button className="btn ghost" onClick={reload}>Refresh</button>
    </div>

    {tab==="actions"&&<div className="card premium">
      <div className="cardTitle"><h3>Pending Actions</h3><span className="badge amber">{pending.length}</span></div>
      <div className="table manager-table"><table><thead><tr><th>Date</th><th>Action</th><th>Employee</th><th>Manager</th><th>Status</th></tr></thead><tbody>{actions.map(a=><tr key={(a.action_type||"action")+(a.action_id||a.id||a.employee_id)}><td>{fmtDate(a.action_date||a.due_date||a.created_at)}</td><td><b>{a.title||humanLabel(a.action_type)}</b><br/><span>{humanLabel(a.action_type||a.request_type||"action")}</span></td><td>{a.employee_code||"—"} · {a.full_name||a.employee_name||"—"}</td><td>{a.manager_name||"—"}</td><td><StatusBadge value={a.status}/></td></tr>)}</tbody></table></div>
      {!actions.length&&<div className="empty">No pending actions found, or manager action view is not installed yet.</div>}
    </div>}

    {tab==="team"&&<div className="card premium">
      <div className="cardTitle"><h3>My Team / Org View</h3><span className="badge">{team.length}</span></div>
      <div className="team-card-grid">{team.map(t=><div className="team-mini-card" key={t.employee_id||t.employee_code}><Avatar name={t.full_name} path={t.photo_path}/><div><b>{t.full_name}</b><span>{t.employee_code} · {t.designation_name||"Designation missing"}</span><em>{t.department_code||t.department_name||"Department missing"} · {t.relationship_type||"primary_manager"}</em></div><StatusBadge value={t.status||"active"}/></div>)}</div>
      {!team.length&&<div className="empty">No team rows found. Install v_manager_my_team / employee_relationships views for manager org visibility.</div>}
    </div>}

    {tab==="interviews"&&<div className="card premium">
      <div className="cardTitle"><h3>Interview Tasks</h3><span className="badge blue">{interviews.length}</span></div>
      <div className="table manager-table"><table><thead><tr><th>When</th><th>MRF</th><th>Role</th><th>Candidate</th><th>Status</th></tr></thead><tbody>{interviews.map(i=><tr key={i.interview_round_id||i.id}><td>{fmtDate(i.scheduled_at)}</td><td>{i.mrf_reference_no||"—"}</td><td>{i.role_title||"—"}</td><td>{i.candidate_name||"—"}</td><td><StatusBadge value={i.status}/></td></tr>)}</tbody></table></div>
      {!interviews.length&&<div className="empty">Interview workflow tables/views are not installed yet. Use Assessment Builder meanwhile.</div>}
    </div>}

    {tab==="assessment"&&<div className="card premium">
      <div className="cardTitle"><h3>Interview Question & Assessment Builder</h3><span className="badge goldb">Manager tool</span></div>
      <div className="formGrid">
        <TextInput field="mrf_reference" label="MRF Reference Number" value={mrfRef} onChange={setMrfRef}/>
        <TextInput field="role_title" label="Role / Position" value={roleTitle} onChange={setRoleTitle}/>
      </div>
      <div className="toolbar"><button className="btn gold" onClick={generateInterviewQuestions}>Generate Interview Questions</button><button className="btn ghost" onClick={copyInterviewPack} disabled={!questionSet.length}>Copy Pack for HR</button></div>
      {!!questionSet.length&&<div className="question-list">{questionSet.map((q,i)=><div key={i}><b>Q{i+1}</b><span>{q}</span></div>)}</div>}
      <label className="full"><span className="muted">Assessment Notes / Recommendation</span><textarea className="input" style={{minHeight:140}} value={assessment} onChange={e=>setAssessment(e.target.value)} placeholder="Functional knowledge, thinking, execution, ownership, communication, culture fit, risks, recommendation..."/></label>
      <p className="muted">Next build will save this to interview_assessments and route to HR Admin for offer workflow.</p>
    </div>}
  </div>
}

function EmployeePortalDashboard({profile}){
  const {data,loading,error}=useLoad(async()=>{const [d,n,e]=await Promise.all([supabase.from('v_employee_portal_dashboard').select('*').limit(1),supabase.from('notification_queue_v2').select('*').order('scheduled_for',{ascending:true}).limit(20),supabase.from('v_employee_events_calendar').select('*').order('event_date',{ascending:true}).limit(20)]); for(const r of [d,n,e]) if(r.error) throw r.error; return {dash:(d.data||[])[0],notifs:n.data||[],events:e.data||[]}},[]);
  if(loading)return <div className="empty">Loading your dashboard...</div>; if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  const d=data.dash||{};
  return <div className="grid"><div className="grid4"><Stat label="Today Inside Minutes" value={d.total_inside_minutes||0} icon="◎"/><Stat label="Pending Leaves" value={d.pending_leave_count||0} icon="◷"/><Stat label="Regularizations" value={d.pending_regularization_count||0} icon="✎"/><Stat label="Upcoming Events" value={d.upcoming_event_count||0} icon="📅"/></div><div className="card premium"><h3>My Status</h3><p><b>{d.full_name}</b> · {d.employee_code}</p><p className="muted">{d.designation_name||'Designation not mapped'} · {d.department_code||'Department not mapped'} · {d.site_code||'Site not mapped'}</p><p>Confirmation: <StatusBadge value={d.confirmation_status}/></p><p>Due date: {fmtDate(d.current_confirmation_due_date)}</p></div><div className="card premium"><h3>Upcoming Events</h3>{data.events.map(e=><div className="mini-row" key={e.event_id}><span>{fmtDate(e.event_date)}</span><b>{e.event_title}</b><StatusBadge value={e.event_status}/></div>)}</div></div>
}
function EmployeeLeave({profile}){
  const {data,loading,error,reload}=useLoad(async()=>{const [b,t,a]=await Promise.all([supabase.from('v_employee_leave_balances_explained').select('*').order('leave_code'),supabase.from('leave_types').select('*').eq('is_active',true).order('display_order'),supabase.from('leave_applications').select('*,leave_types(leave_code,leave_name)').order('submitted_at',{ascending:false}).limit(50)]); for(const r of [b,t,a]) if(r.error) throw r.error; return {balances:b.data||[],types:t.data||[],apps:a.data||[]}},[]);
  if(loading)return <Loading label="Leave"/>; if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  return <div className="grid"><div className="grid4">{data.balances.slice(0,4).map(b=><Stat key={b.leave_code} label={`${b.leave_code} Balance`} value={b.current_balance} icon="◷"/>)}</div><div className="card premium"><h3>Balance Explanation</h3>{data.balances.map(b=><div className="explain" key={b.leave_code}><b>{b.leave_name} ({b.leave_code})</b><p>{b.explanation_text}</p></div>)}</div><div className="card premium"><h3>Recent Applications</h3><div className="table"><table><thead><tr><th>Type</th><th>Dates</th><th>Days</th><th>Status</th></tr></thead><tbody>{data.apps.map(a=><tr key={a.id}><td>{a.leave_types?.leave_code||a.leave_type_id}</td><td>{fmtDate(a.start_date)} to {fmtDate(a.end_date)}</td><td>{a.calculated_debit_days||a.requested_days}</td><td><StatusBadge value={a.status}/></td></tr>)}</tbody></table></div></div></div>
}
function ApplyLeave({profile}){
  const {data,loading,error}=useLoad(async()=>{const r=await supabase.from('leave_types').select('*').eq('is_employee_selectable',true).eq('is_active',true).order('display_order'); if(r.error)throw r.error; return r.data||[]},[]);
  const [form,setForm]=useState({leave_type_id:'',start_date:today(),end_date:today(),reason:''});
  const [leaveMsg,setLeaveMsg]=useState(null);
  if(loading)return <Loading label="Leave Types"/>; if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  async function submit(){if(!form.leave_type_id){setLeaveMsg({type:"error",text:"Please select a leave type."});return;} const {error}=await supabase.rpc('rpc_apply_leave',{p_leave_type_id:form.leave_type_id,p_start_date:form.start_date,p_end_date:form.end_date,p_reason:form.reason}); if(error){setLeaveMsg({type:"error",text:error.message});}else{setLeaveMsg({type:"success",text:"Leave submitted successfully. You can track it under My Leave."});setForm({leave_type_id:"",start_date:today(),end_date:today(),reason:""});}}
  return <div className="card premium"><h3>Apply Leave</h3>
    {leaveMsg&&<div style={{padding:"10px 14px",marginBottom:12,borderRadius:10,background:leaveMsg.type==="success"?"var(--green-t)":"var(--red-t)",color:leaveMsg.type==="success"?"var(--green)":"var(--red)"}}>{leaveMsg.text} <button onClick={()=>setLeaveMsg(null)} style={{marginLeft:8,border:0,background:"transparent",cursor:"pointer"}}>✕</button></div>}<div className="formGrid"><label>Leave Type<select className="input" value={form.leave_type_id} onChange={e=>setForm({...form,leave_type_id:e.target.value})}><option value="">Select</option>{data.map(t=><option key={t.id} value={t.id}>{t.leave_code} - {t.leave_name}</option>)}</select></label><TextInput field="start_date" label="Start Date" type="date" value={form.start_date} onChange={v=>setForm({...form,start_date:v})}/><TextInput field="end_date" label="End Date" type="date" value={form.end_date} onChange={v=>setForm({...form,end_date:v})}/><TextInput field="reason" label="Reason" value={form.reason} onChange={v=>setForm({...form,reason:v})}/></div><button className="btn gold" onClick={submit}>Submit Leave</button></div>
}
function EmployeeAttendance(){const {data,loading,error}=useLoad(async()=>{const r=await supabase.from('v_employee_attendance_self_view').select('*').order('work_date',{ascending:false}).limit(60); if(r.error)throw r.error; return r.data||[]},[]); if(loading)return <div className="empty">Loading attendance...</div>; if(error)return <div className="card premium"><p className="error">{error}</p></div>; return <div className="card premium"><h3>My Attendance</h3><div className="table"><table><thead><tr><th>Date</th><th>First IN</th><th>Last OUT</th><th>Inside</th><th>Status</th><th>Regularization</th></tr></thead><tbody>{data.map(r=><tr key={r.work_date}><td>{fmtDate(r.work_date)}</td><td>{r.first_in?new Date(r.first_in).toLocaleTimeString(): '—'}</td><td>{r.last_out?new Date(r.last_out).toLocaleTimeString(): '—'}</td><td>{r.total_inside_minutes||0} min</td><td>{r.raw_status||'—'}</td><td><StatusBadge value={r.regularization_status}/></td></tr>)}</tbody></table></div></div>}
function EmployeeNotifications(){const {data,loading,error}=useLoad(async()=>{const r=await supabase.from('notification_queue_v2').select('*').order('scheduled_for',{ascending:true}).limit(100); if(r.error)throw r.error; return r.data||[]},[]); if(loading)return <div className="empty">Loading notifications...</div>; if(error)return <div className="card premium"><p className="error">{error}</p></div>; return <div className="card premium"><h3>My Notifications</h3><div className="table"><table><thead><tr><th>When</th><th>Channel</th><th>Status</th><th>Subject</th></tr></thead><tbody>{data.map(n=><tr key={n.id}><td>{fmtDate(n.scheduled_for)}</td><td>{n.channel}</td><td><StatusBadge value={n.status}/></td><td>{n.subject_rendered||n.body_rendered}</td></tr>)}</tbody></table></div></div>}
function EmployeeProfileSelf(){const {data,loading,error}=useLoad(async()=>{const r=await supabase.from('v_employee_portal_dashboard').select('*').limit(1); if(r.error)throw r.error; return (r.data||[])[0]||{}},[]); if(loading)return <div className="empty">Loading profile...</div>; if(error)return <div className="card premium"><p className="error">{error}</p></div>; return <div className="card premium"><h3>My Profile</h3><dl className="profile-dl"><dt>Name</dt><dd>{data.full_name}</dd><dt>Employee Code</dt><dd>{data.employee_code}</dd><dt>Department</dt><dd>{data.department_code}</dd><dt>Site</dt><dd>{data.site_code}</dd><dt>Designation</dt><dd>{data.designation_name}</dd><dt>Confirmation</dt><dd>{data.confirmation_status} · due {fmtDate(data.current_confirmation_due_date)}</dd></dl></div>}
function EmployeeRequests({profile}){
  const {data,loading,error,reload}=useLoad(async()=>{
    const [leaves,regs]=await Promise.all([
      supabase.from('leave_applications').select('*,leave_types(leave_code,leave_name)').order('submitted_at',{ascending:false}).limit(50),
      supabase.from('attendance_regularization_requests').select('*').order('submitted_at',{ascending:false}).limit(50)
    ]);
    if(leaves.error)throw leaves.error;
    return {leaves:leaves.data||[],regs:regs.data||[]};
  },[]);
  if(loading)return <Loading label="My Requests"/>;
  if(error)return <div className="card premium"><p className="error">{error}</p></div>;
  return <div className="grid">
    <div className="card premium">
      <div className="cardTitle"><h3>My Leave Applications</h3><span className="badge">{data.leaves.length}</span></div>
      <div className="table"><table><thead><tr><th>Type</th><th>From</th><th>To</th><th>Days</th><th>Submitted</th><th>Status</th></tr></thead>
      <tbody>{data.leaves.map(a=><tr key={a.id}>
        <td>{a.leave_types?.leave_code||'—'} · {a.leave_types?.leave_name||'—'}</td>
        <td>{fmtDate(a.start_date)}</td><td>{fmtDate(a.end_date)}</td>
        <td>{a.calculated_debit_days||a.requested_days||'—'}</td>
        <td>{fmtDate(a.submitted_at)}</td>
        <td><StatusBadge value={a.status}/></td>
      </tr>)}</tbody></table></div>
    </div>
    <div className="card premium">
      <div className="cardTitle"><h3>My Regularization Requests</h3><span className="badge">{data.regs.length}</span></div>
      <div className="table"><table><thead><tr><th>Date</th><th>Type</th><th>Requested Time</th><th>Reason</th><th>Status</th></tr></thead>
      <tbody>{data.regs.map(r=><tr key={r.id}>
        <td>{fmtDate(r.work_date)}</td>
        <td>{r.request_type}</td>
        <td>{r.requested_in_time?`IN ${timeOnly(r.requested_in_time)}`:''} {r.requested_out_time?`OUT ${timeOnly(r.requested_out_time)}`:''}</td>
        <td>{r.reason_text}</td>
        <td><StatusBadge value={r.status}/></td>
      </tr>)}</tbody></table></div>
    </div>
  </div>
}

function Employee({page,profile}){
  const map={
    "my-dashboard":<EmployeePortalDashboard profile={profile}/>,
    "my-access":<MyAccessPrivacy profile={profile}/>,
    "my-attendance":<EmployeeAttendance profile={profile}/>,
    "my-leaves":<EmployeeLeave profile={profile}/>,
    "apply-leave":<ApplyLeave profile={profile}/>,
    "my-requests":<EmployeeRequests profile={profile}/>,
    "my-notifications":<EmployeeNotifications profile={profile}/>,
    "my-profile":<EmployeeProfileSelf profile={profile}/>
  };
  return map[page]||<EmployeePortalDashboard profile={profile}/>;
}

function parseCsv(text){
  const lines=[]; let row=[], cur="", q=false;
  for(let i=0;i<text.length;i++){
    const c=text[i], n=text[i+1];
    if(c==='"' && q && n==='"'){cur+='"'; i++; continue}
    if(c==='"'){q=!q; continue}
    if(c===',' && !q){row.push(cur); cur=""; continue}
    if((c==='\n'||c==='\r') && !q){ if(c==='\r' && n==='\n') i++; row.push(cur); if(row.some(x=>String(x).trim()!=="")) lines.push(row); row=[]; cur=""; continue}
    cur+=c;
  }
  row.push(cur); if(row.some(x=>String(x).trim()!=="")) lines.push(row);
  const headers=(lines.shift()||[]).map(h=>String(h).trim());
  return lines.map(r=>Object.fromEntries(headers.map((h,i)=>[h,String(r[i]??"").trim()])));
}

function downloadCsv(filename,rows){if(!rows||!rows.length){console.warn("downloadCsv: empty rows for",filename);return;}const csv=[Object.keys(rows[0]||{}).join(","),...rows.map(r=>Object.values(r).map(v=>`"${String(typeof v==="object"?JSON.stringify(v):v??"").replaceAll('"','""')}"`).join(","))].join("\n");const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"}));a.download=filename;a.click();}

createRoot(document.getElementById("root")).render(<App/>);

