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}) => {children}; 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=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()=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{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??:null; } function FieldLabel({field,children,required=false}){ return ; } 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
{label}
{if(!disabled){window.dispatchEvent(new CustomEvent("smart-select-open",{detail:myId}));setOpen(true);setQ("")}}} onChange={e=>{setQ(e.target.value);setOpen(true)}}/> {value&&}
{open&&!disabled&&
{list.length} option{list.length===1?"":"s"} {q?`matching “${q}”`:"available"} · type to narrow
{list.map((o,i)=>)} {!list.length&&
No matching option. Add it under Setup Data first.
}
}
} function TextInput({field,value,onChange,type="text",required=false,placeholder=""}){ return
onChange(e.target.value)}/>
} 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?:
{init(name)}
} 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
Loading Crewpad…
; if(!session)return ; if(!profile || !rolesLoaded)return
Loading your access profile…
; if(!profile)return
CP

Profile not linked

This auth user needs a profile and role.

; 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 ; } 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
Crewpad
CP

HR
Operations

Attendance intelligence, gate control, leave management, visitor tracking, and emergency readiness — unified.

{["Live gate punch terminal","Attendance & regularisation","Leave lifecycle management","Visitor & emergency management"].map(f=>
{f}
)}

Welcome back

Sign in with your HR-issued username or email
setEmail(e.target.value)} placeholder="rohan.admin or guard.p77.main" autoComplete="username"/> setPassword(e.target.value)} placeholder="••••••••" autoComplete="current-password"/> {err&&

{err}

}
{TENANT_CODE} · Powered by Crewpad
} 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

{title}

{portal.toUpperCase()} portal · Asia/Kolkata

} 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
Checking access context...
; if(error)return
Access context unavailable: {error.message||String(error)}
; const ok=data?.ok, ctx=data?.context||{}; if(!ok)return
{data?.access_block_reason||'Employee profile not linked'} · No fallback employee context will be used.
; return
{ctx.full_name} · {ctx.employee_code} · {ctx.portal_status} · {ctx.permissions?.length||0} permissions
; } 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
Loading Access & Privacy...
; if(error)return

Access & Privacy failed to load

{error.message||String(error)}

Run SQL 26 first.

; 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

Identity, Access & Privacy Governance

Deny by default · role + scope permissions · HR view-as audited · mail dormant

{accessErr&&
{accessErr}
} {viewAsSuccess&&
{viewAsSuccess}
}
{tabs.map(t=>)}
{tab==='provisioning'&&
setQ(e.target.value)}/>
{rows.map(r=>)}
EmployeeOrgLoginStatusScopesActions
{r.employee_code}
{r.full_name}
{r.official_email||'No official email'}
{r.department_code||'—'}
{r.site_code||'Site?'}
Mgr: {r.reporting_manager_name||'—'}
{r.login_id||'Not generated'}
{r.username||''}

{r.access_enabled?'Access enabled':'Access disabled'}
SelfManagerReporteesDeptSiteHRGuard
} {tab==='permissions'&&

Edit Access

{!selected?

Select an employee from Provisioning first.

:<>

{selected.full_name} · {selected.employee_code}

}

Permission Catalogue

{data.perms.map(p=>)}
PermissionGroupScopeSensitive
{p.permission_code}
{p.description}
{p.permission_group}{p.default_scope_type}{p.is_sensitive?'Yes':'No'}
} {tab==='gaps'&&

Access Review Gaps

Use this to catch wrong employee context, exited users with access, broad manager/HR/guard access, and missing self-view.

{data.gaps.map(g=>)}
EmployeeGaps
{g.employee_code}
{g.full_name}
{(g.access_gaps||[]).map(x=>{x})}
} {tab==='privacy'&&

Privacy Notices

{data.privacy.map(p=>)}
NoticeVersionStatusEffective
{p.notice_title}
{p.notice_code}
{p.version_no}{fmtDate(p.effective_from)}

Acknowledgement Pending

{data.acks.filter(x=>x.acknowledgement_pending).slice(0,200).map(a=>)}
EmployeeNoticeStatus
{a.employee_code}
{a.full_name}
{a.notice_title} {a.version_no}Pending
} {tab==='view-as'&&

HR View-As Employee

This is not impersonation. HR remains HR. A reason is required and each session is audited.

Select an employee from Provisioning and click View As.

{selected&&}

Recent View-As Logs

{data.viewas.map(v=>)}
TargetReasonStatusStarted
{v.target_employee_id}{v.reason}{fmtDateTime(v.started_at)}
} {tab==='audit'&&

Access Audit Logs

{data.audit.map(a=>)}
WhenActionTargetReason
{fmtDateTime(a.created_at)}{a.action_type}
{a.action_summary}
{a.target_employee_id||a.target_user_id||'—'}{a.reason||'—'}
} {tab==='debug'&&

Identity Debug

{JSON.stringify(data.ctx,null,2)}
}
} 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
Loading access details...
; if(error)return

{error.message||String(error)}

; 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

Access Blocked

{data.ctx?.access_block_reason||'Employee profile not linked'}

No fallback employee context is used. Contact HR to link your employee profile.

; return

My Access

{ctx.full_name} · {ctx.employee_code}

Portal status: {ctx.portal_status} · Access enabled: {String(ctx.access_enabled)}

{(ctx.permissions||[]).map(p=>{p})}

Privacy Notice

{pending?<>

Privacy acknowledgement pending.

:

No pending privacy acknowledgement.

} {notice&&
{notice.notice_title} · {notice.version_no}

{notice.notice_text}

}
} function Router({portal,page,profile,roles}){ if(portal==="guard")return ; if(portal==="employee")return ; const map={ dashboard:, employees:, access:, orgchart:, bulk:, users:, visitors:, lifecycle:, confirmations:, events:, reminders:, manager:, approvals:, absentees:, reports:, emergency:, setup:, audit: }; return map[page]||null; } function Stat({label,value,icon="↯",tone=""}){ return
{icon&&
{icon}
}
{value}
{label}
} function Loading({label=""}){ return
{label ? `Loading ${label}…` : "Loading…"}
} /* 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 ; if(error)return

{error}

; return

HR Operations Control Room

Live attendance, visitor presence, approvals, absentee risk and emergency readiness from Supabase operational views.
Live data connected
{data.inside.length}
Inside Now
{data.visitors.length}
Visitors
{data.approvals.length}
Approvals
{data.abs.length}
Unresolved
{Object.entries(data.bySite).map(([code,count])=>

{code} Occupancy

{count}
employees currently inside
)} {Object.keys(data.bySite).length===0&&

No Site Data

0
No employees inside yet
}

Pending Attendance Approvals

{data.approvals.length}
{data.approvals.length?
{data.approvals.map(a=>)}
EmployeeTypeDateRequested
{a.employee_code} — {a.full_name}{a.request_type}{a.work_date}{a.requested_in_time?timeOnly(a.requested_in_time):""} {a.requested_out_time?timeOnly(a.requested_out_time):""}
:
No pending approvals.
}

Visitor Overstay / Active Visitors

{data.visitors.length}
{data.visitors.map(v=>
{init(v.visitor_name)}
{v.visitor_name}
{v.organization||"—"} · {v.purpose||"—"} · Host: {v.host_employee_name||v.host_name||"—"} · Inside: {runTime(v.entry_time)}
{v.overstay_flag?"Overstay":"Inside"}
)}

Unresolved Absentees

{data.abs.length}
{data.abs.map(a=>
{init(a.employee?.full_name)}
{a.employee?.employee_code} — {a.employee?.full_name}
{a.absence_date} · {a.remarks}
)}

Emergency Roles

{data.roster.length}
{data.roster.slice(0,12).map((r,i)=>{r.employee_code} · {r.emergency_role})}

Recent Exports

{data.exports.length}
{data.exports.map(x=>
{x.report_type}
{x.file_name} · {fmt(x.exported_at)}
)}
; } 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 ; if(error)return
{error}
; return

Attendance Regularization Approvals

{data.length}
{data.length?
{data.map(r=>)}
EmployeeRequestWork DateRequested TimeReasonAction
{r.employee_code} — {r.full_name}
{r.department_name||""}
{r.request_type}{r.work_date}{r.requested_in_time?`IN ${timeOnly(r.requested_in_time)}`:""} {r.requested_out_time?`OUT ${timeOnly(r.requested_out_time)}`:""}{r.reason_text}
:
No pending approvals.
}
} 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 ; return

Absentee Resolution

{data.filter(x=>x.status==="unresolved").length} unresolved
{data.map(r=>)}
EmployeeDateStatusCurrent ReasonResolve
{r.employee?.employee_code} — {r.employee?.full_name}{r.absence_date}{r.status}{r.remarks}{r.status==="unresolved"?
:"—"}
} 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 ; if(error)return
{error}
; const rows=data.filter(v=>`${v.visitor_name} ${v.organization||""} ${v.phone||""}`.toLowerCase().includes(q.toLowerCase())); return
setQ(e.target.value)}/>
{rows.map(v=>

{v.visitor_name}

{v.status}

{v.organization||"—"} · {v.phone||"—"}

Default host: {v.default_host_name||"—"}
Purpose: {v.default_purpose||"—"}
Visits: {v.total_visits}
)}
} 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
Loading emergency readiness...
; if(error)return
{error}
; return
Emergency Muster Readiness
{data.inside.length + data.visitors.length}
people currently on premises

Emergency Roster

{data.roster.length}
{data.roster.map((r,i)=>)}
EmployeeRoleSiteDept
{r.employee_code} — {r.full_name}{r.emergency_role}{r.site_code}{r.department_name}

Emergency Contacts

{data.contacts.length}
{data.contacts.map(c=>
{c.contact_name}
{c.contact_role} · {c.phone} · {c.email}
)}
} 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 ; if(error)return

{error}

If this says v_attendance_daily_movement_report is missing, run SQL 06 first.
; 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
setDate(e.target.value)}/>

Daily Time Summary

{totals.missing} missing final OUT
{data.summary.map(r=>)}
EmployeeDeptSiteFirst INLast OUTInsideOutside / FreeBreakExitsFlags
{r.employee_code} — {r.full_name} {r.department_name||"—"} {r.site_code||"—"} {timeOnly(r.first_in)} {timeOnly(r.last_out)} {r.total_inside_hhmm} {r.total_outside_hhmm} {r.total_break_hhmm} {r.exit_count} {r.missing_final_out?Missing OUT:r.currently_inside_as_of_last_event?Inside:"—"}

Raw Movement Log

{data.events.length}
{data.events.map(e=>)}
TimeEmployeeEventReasonSourceSite
{fmt(e.event_time)} {e.employee?.employee_code} — {e.employee?.full_name} {String(e.event_type)} {e.reason_code||e.reason_text||"—"} {String(e.source)} {e.site?.site_code||"—"}
} 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
Loading audit/export logs...
; return

Report Export Logs

{data.length}
{data.map(x=>)}
TimeReportFormatRowsFile
{fmt(x.exported_at)}{x.report_type}{x.export_format}{x.row_count}{x.file_name}
} 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 ; if(error)return

{error}

; 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
Setup Control Center Configure masters without touching SQL.
{sections.map(s=>)}

{sec.title}

{sec.desc}

{active==="orgs"&&
{data.orgs?.length||0}Total Nodes
{Object.entries(orgTypeCounts).map(([k,v])=>
{v}{typeBadgeLabel(k)}
)}
} setQ(e.target.value)}/> {editing&&

{editing.new?"Add":"Edit"} {sec.title}

{active==="orgs"&&
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.
}
{sec.fields.map(field=>{ const val=form[field]??""; if(field==="child_count")return
; if(field==="org_unit_type")return 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 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 setForm({...form,[field]:v})} options={data.departments.map(d=>({...d,code:d.department_code,label:d.department_name}))}/>; if(field==="track")return 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 ; return setForm({...form,[field]:v})}/>; })}
{setupErr&&
{setupErr}
}
}
{sec.fields.map(f=>)}{active==="orgs"&&}{filtered.map(row=> {sec.fields.map(f=>)} {active==="orgs"&&} )}
{humanLabel(f)} PathActions
{f==="org_unit_type"?{typeBadgeLabel(valueFor(row,f))}:String(valueFor(row,f)??"")}{row.pathLabel}
{active==="orgs"&&}
} 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 setFailed(true)} loading="lazy"/>; return
{init(name)}
; } 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)=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

Org Chart could not render

{String(this.state.error?.message||this.state.error)}

The data route is working. This is a frontend render issue. Use v1.5.2 patch and refresh.

} return this.props.children; } } function OrgStudioRoute(){return } 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
Loading Org Studio...
; if(error)return

Org Studio failed to load

{error}

Run SQL 23 first, then hard refresh.

; 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
{e.preventDefault();setCtx({x:e.clientX,y:e.clientY,node:n})}} onClick={()=>setSelected(n)}>
{view!=="compact"&&cardSize!=="micro"&&}
{view==="people"?(n.full_name||"Vacant"):(n.position_title||"Position")} {view==="people"?n.position_title:(n.full_name||n.position_status)} {n.position_code} · {n.site_code||"Site?"}
{lensText(lens,n)} {kids.length>0&&{kids.length} below} {n.open_comment_count>0&&{n.open_comment_count} comment}
{kids.length>0&&!isCollapsed&&depth+1{kids.map(c=>card(c,depth+1))}
} {kids.length>0&&!isCollapsed&&depth+1>=maxDepth&&}
} const tabs=["workspace","explorer","positions","vacancies","gaps","versions","exports","debug"]; return
setCtx(null)}>

Crewpad Org Studio

Design your organization before changing your organization.

{loadWarnings.length>0&&

Load warning: {loadWarnings[0]}

}
{summary.total_positions||nodesAll.length}Positions
{summary.occupied_positions||0}Occupied
{summary.vacant_positions||0}Vacant
{gaps.length}Gaps
setQ(e.target.value)} placeholder="employee, position, grade, site..."/>
{tabs.map(t=>)}
{tab==="workspace"&&

Navigator

Roots: {derivedRootCount} · Main branch: {mainRoot?countDescendants(mainRoot)+1:0} positions

{rootsRanked.map(r=>)}
{orgMode==="departments"&&
{departmentGroups.map(g=>
{g.key}{g.nodes.length} positions
{g.nodes.slice(0,12).map(n=>)}
)}
} {orgMode!=="departments"&&
{Math.round(zoom*100)}% Main Tree shows largest branch · use Tree Mode for all/orphans
} {orgMode!=="departments" && (nodesAll.length===0?
No Org Studio data.
Run SQL 28 sync, then refresh.
:visibleRoots().length===0?
Org data loaded but no root could be derived.
Nodes: {nodesAll.length} · Edges: {edges.length}. Open Debug tab and check edge field names.
:
setDragging(null)} onMouseLeave={()=>setDragging(null)} onWheel={handleCanvasWheel}>
{visibleRoots().map(r=>card(r))}
)}

Details

CanvasZoom {Math.round(zoom*100)}%X {Math.round(pan.x)} · Y {Math.round(pan.y)}
{!selected?

Click a card to see position details.

:<>
{selected.position_title}{selected.full_name||selected.position_status}
Position Code
{selected.position_code}
Status
{selected.position_status}
Org Unit
{selected.org_unit_name||"—"}
Site
{selected.site_code||"—"}
Band / Grade
{selected.band||"—"} / {selected.grade||"—"}
Parent
{selected.parent_position_title||"—"}
}
} {tab==="explorer"&&
Drag empty canvas to pan
setDragging(null)} onMouseLeave={()=>setDragging(null)} onWheel={handleCanvasWheel}>
{visibleRoots().map(r=>card(r))}
} {tab==="positions"&&
{nodesAll.filter(filterNode).map(n=>)}
PositionOccupantStatusParentOrg UnitSiteBandGradeActions
{n.position_title}
{n.position_code}
{n.full_name||"—"}{n.position_status}{n.parent_position_title||"—"}{n.org_unit_name||"—"}{n.site_code||"—"}{n.band||"—"}{n.grade||"—"}
} {tab==="vacancies"&&
{nodesAll.filter(n=>["vacant","planned","frozen"].includes(n.position_status)).map(n=>)}
PositionStatusCriticalityHiringTarget DOJOrg UnitSite
{n.position_title}
{n.position_code}
{n.position_status}{n.criticality}{n.hiring_status||"—"}{n.target_joining_date||"—"}{n.org_unit_name||"—"}{n.site_code||"—"}
} {tab==="gaps"&&
{gaps.map(g=>)}
PositionEmployeeGaps
{g.position_title}
{g.position_code}
{g.full_name||"—"}{(g.gaps||[]).map(x=>{x})}
} {tab==="versions"&&
{versions.map(v=>)}
VersionStatusPurposeDesign PrincipleCreated
{v.version_name}
{v.version_code}
{v.status}{v.purpose||"—"}{v.design_principle||"—"}{new Date(v.created_at).toLocaleString()}
} {tab==="exports"&&

Exports / Review Pack

Use these for leadership reviews and PowerPoint/SmartArt creation.

{makeHierarchyText(view==="people"?"people":"position")}
} {tab==="debug"&&

Debug Center

Use this if the module appears blank or counts look wrong.

{versions.length}Versions
{nodesAll.length}Visible Nodes
{edges.length}Edges
{loadWarnings.length}Warnings
{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)}
} {editOpen&&
setEditOpen(false)}>
e.stopPropagation()}>

Edit Position