Cyber Crime Investigation Dashboard

Gujarat Police · CCOE
Secure sign-in
Forgot Password?

Reset Password

Step 1: Identity Verification
A One Time Password (OTP) will be sent to the registered email address (abc@xyz.com). It is valid for 10 minutes.
← Back to Login
CC

Cyber Crime Investigation — Dashboard

Gujarat Police · Cyber Centre of Excellence
Saved on this device
IO —
Zone
Stage
Type
Status
FIR No. Crime Type Investigating Officer (PI) Zone Stage Progress Days / FIR Status
\",\"crimeType\":\"Online Financial Fraud\",\"piName\":\"PI B B GOYAL\",\"zone\":\"Gandhinagar\",\"firDate\":\"2026-09-18\",\"lastUpdate\":\"2026-09-18\",\"lossAmount\":0,\"frozenAmount\":0,\"recoveryAmount\":0,\"notes\":\"\",\"checks\":{},\"instanceCounts\":{},\"instanceLabels\":{},\"instanceDates\":{},\"instanceCS\":{},\"instanceLimit\":{},\"instanceBail\":{},\"suspectCount\":1,\"remarks\":[],\"remarksDone\":false}]","gp_ccoe_seeded_v1":"[\"seed_fir1724\",\"seed_fir1025\"]","gp_ccoe_stage_merge_v6":"2026-09-05T10:41:46+00:00","gp_ccoe_unescape_v7":"2026-09-07T16:17:16+00:00 (cases repaired: 1)"}; window.__CCOE_SERVER_USERS = [{"id":1,"username":"admin","role":"admin","name":"Administrator","zone":"","is_active":1,"pass":""},{"id":30,"username":"sp.rjhala","role":"sp","name":"DR.Rajdeepsinh Jhala","zone":"","is_active":1,"pass":""},{"id":33,"username":"igp.bipin.ahire","role":"ig","name":"IGP Bipin Ahire","zone":"","is_active":1,"pass":""},{"id":31,"username":"sp.skeshvala","role":"sp","name":"SP Sanjay Keshvala ","zone":"","is_active":1,"pass":""},{"id":32,"username":"sp.vpbheda","role":"sp","name":"SP Vivek Bheda ","zone":"","is_active":1,"pass":""},{"id":25,"username":"abchaudhary.bor","role":"io","name":"PI A B CHAUDHARY","zone":"Border","is_active":1,"pass":""},{"id":26,"username":"adpuvar.bor","role":"io","name":"PI A D PUVAR","zone":"Border","is_active":1,"pass":""},{"id":28,"username":"cvgosai.bor","role":"io","name":"PI C V GOSAI","zone":"Border","is_active":1,"pass":""},{"id":29,"username":"mvpatel.bor","role":"io","name":"PI M V PATEL","zone":"Border","is_active":1,"pass":""},{"id":27,"username":"pgpanara.bor","role":"io","name":"PI P G PANARA","zone":"Border","is_active":1,"pass":""},{"id":6,"username":"bbgoyal.gnr","role":"io","name":"PI B B GOYAL","zone":"Gandhinagar","is_active":1,"pass":""},{"id":3,"username":"cldesai.gnr","role":"io","name":"PI C L DESAI","zone":"Gandhinagar","is_active":1,"pass":""},{"id":8,"username":"kbdesai.gnr","role":"io","name":"PI K B DESAI","zone":"Gandhinagar","is_active":1,"pass":""},{"id":4,"username":"kspatel.gnr","role":"io","name":"PI K S PATEL","zone":"Gandhinagar","is_active":1,"pass":""},{"id":2,"username":"pdmakwana.gnr","role":"io","name":"PI P D MAKWANA","zone":"Gandhinagar","is_active":1,"pass":""},{"id":120644,"username":"pcchauhan.gnr","role":"io","name":"PI PC CHAUHAN","zone":"Gandhinagar","is_active":1,"pass":""},{"id":5,"username":"vajogarana.gnr","role":"io","name":"PI V A JOGARANA","zone":"Gandhinagar","is_active":1,"pass":""},{"id":7,"username":"ysgamit.gnr","role":"io","name":"PI Y S GAMIT","zone":"Gandhinagar","is_active":1,"pass":""},{"id":19,"username":"abchaudhary.sau","role":"io","name":"PI A B CHAUDHARY","zone":"Saurashtra","is_active":1,"pass":""},{"id":14,"username":"bpmeghalatar.sau","role":"io","name":"PI B P MEGHALATAR","zone":"Saurashtra","is_active":1,"pass":""},{"id":17,"username":"bppatel.sau","role":"io","name":"PI B P PATEL","zone":"Saurashtra","is_active":1,"pass":""},{"id":15,"username":"pmjudal.sau","role":"io","name":"PI P M JUDAL","zone":"Saurashtra","is_active":1,"pass":""},{"id":13,"username":"rbvihol.sau","role":"io","name":"PI R B VIHOL","zone":"Saurashtra","is_active":1,"pass":""},{"id":18,"username":"rrsolanki.sau","role":"io","name":"PI R R SOLANKI","zone":"Saurashtra","is_active":1,"pass":""},{"id":16,"username":"smsoni.sau","role":"io","name":"PI S M SONI","zone":"Saurashtra","is_active":1,"pass":""},{"id":22,"username":"acgohil.sur","role":"io","name":"PI A C GOHIL","zone":"Surat","is_active":1,"pass":""},{"id":24,"username":"drbhadarka.sur","role":"io","name":"PI D R BHADARKA","zone":"Surat","is_active":1,"pass":""},{"id":20,"username":"pkgohil.sur","role":"io","name":"PI P K GOHIL","zone":"Surat","is_active":1,"pass":""},{"id":23,"username":"pmvala.sur","role":"io","name":"PI P M VALA","zone":"Surat","is_active":1,"pass":""},{"id":21,"username":"tkdevmurari.sur","role":"io","name":"PI T K DEVMURARI","zone":"Surat","is_active":1,"pass":""},{"id":12,"username":"aachaudhary.vad","role":"io","name":"PI A A CHAUDHARY","zone":"Vadodara","is_active":1,"pass":""},{"id":9,"username":"asasari.vad","role":"io","name":"PI A S ASARI","zone":"Vadodara","is_active":1,"pass":""},{"id":11,"username":"gbdodia.vad","role":"io","name":"PI G B DODIA","zone":"Vadodara","is_active":1,"pass":""},{"id":10,"username":"hbgadhvi.vad","role":"io","name":"PI H B GADHVI","zone":"Vadodara","is_active":1,"pass":""}]; window.__CCOE_SERVER_SESSION = null; /* ===================== WORKFLOW ===================== */ const STAGES = [ { id:"complaint", name:"Complaint", sections:[ { title:"Complaint Registration", items:["Complaint received from victim","NCRP / Cyber Portal complaint verified","FIR registered","Applicable sections of law added","Complainant statement recorded","Supporting documents collected","Incident timeline prepared"]}, { title:"Initial Assessment", items:["Type of cyber crime identified","Loss amount determined","Critical evidence identified","Urgent preservation request issued"]}, { title:"Evidence Collection — Victim Side", items:["Mobile phone examined","Screenshots collected","Chat history preserved","Email headers collected","Transaction details obtained","Call records obtained","Bank statement collected","Device seizure memo prepared"]}, ]}, { id:"financial", name:"Financial Trail", sections:[ { title:"Financial Investigation", items:["Beneficiary account identified","Transaction trail mapped","Account freeze request sent","Debit freeze confirmed","Hold amount verified","Wallet details collected","UPI VPA identified","Merchant details obtained","Cryptocurrency linkage checked","Money trail chart prepared"]}, { title:"Bank — Service Provider Request", items:["Notice issued to bank","KYC obtained","Account statement received","Linked accounts identified"]}, ]}, { id:"technical", name:"Technical Trail", sections:[ { title:"Digital Evidence", items:["IP address obtained","Domain information collected","Website details preserved","Social media account details collected","Device forensic imaging completed","Hash values generated","Chain of custody maintained"]}, { title:"Telecom — Service Provider Request", items:["CAF obtained","CDR requested","Subscriber details obtained","IMEI details collected"]}, { title:"Social Media / Email Request", items:["Preservation request sent","Subscriber information requested","Login IP details requested","Device information requested"]}, { title:"Forensic Examination (FSL)", items:["Device sent to FSL","FSL acknowledgement received","Examination report received","Deleted data recovery attempted","Malware analysis conducted","Relevant evidence extracted"]}, ]}, { id:"suspect", name:"Suspect", sections:[ { title:"Suspect Identification", items:["Suspect identified","Technical analysis completed","Physical verification conducted","Surveillance conducted","Local police coordination completed","Criminal history checked"]}, { title:"Search & Seizure", items:["Search warrant obtained","Search conducted","Devices seized","Digital evidence documented","Seizure memo prepared","Witness signatures obtained"]}, ]}, { id:"arrest", name:"Arrest", sections:[ { title:"Arrest & Interrogation", items:["Arrest proposal approved","Arrest completed","Disclosure statement recorded","Additional accused identified","Recovery made","Remand obtained"]}, ]}, { id:"remarks", name:"Remarks", type:"remarks", sections:[] }, { id:"chargesheet", name:"Charge Sheet", sections:[ { title:"Supervisory Review (PI / DySP / SP)", items:["Investigation reviewed","Pending points identified","Recovery status reviewed","Technical gaps reviewed","Legal compliance reviewed","Next action assigned"]}, { title:"Charge Sheet Preparation", items:["Witness statements completed","Documentary evidence compiled","Digital evidence certified","FSL report attached","Financial trail attached","Accused details verified","Charge sheet prepared","Prosecutor consultation completed"]}, { title:"Final Disposal", items:["Charge Sheet filed","Summary Report filed","Undetected Report filed","Property disposal initiated","Victim informed","Case closed"]}, ]}, ]; const STAGE_COLORS = ["#2563c4","#7b3fb8","#b88a1e","#e07b2f","#c2407a","#0f9b8e","#1a9d5f"]; const CRIME_TYPES = ["Online Financial Fraud","Investment Scam","Trading Scam","Digital Arrest","Social Media Crime","Cyber Stalking","Data Theft","Cyber Slavery","APK / Malware Fraud","UPI Fraud","Mule Hunt","Crypto Fraud","Other"]; const ZONES = ["Gandhinagar","Vadodara","Saurashtra","Surat","Border"]; const ZONE_COLORS = {Gandhinagar:"#2563c4",Vadodara:"#7b3fb8",Saurashtra:"#b88a1e",Surat:"#1a9d5f",Border:"#d6483b"}; const ZONE_CODE = {Gandhinagar:"GNR",Vadodara:"VAD",Saurashtra:"SAU",Surat:"SUR",Border:"BOR"}; function itemId(s,sec,i){return `s${s}_${sec}_${i}`;} function totalItems(){let n=0;STAGES.forEach(st=>st.sections.forEach(sc=>n+=sc.items.length));return n;} const TOTAL_ITEMS = totalItems(); const REPEAT_META = { financial:{icon:"💰",word:"financial trail"}, technical:{icon:"🔬",word:"technical trail"}, suspect:{icon:"👤",word:"suspect"}, arrest:{icon:"🚔",word:"arrest"}, chargesheet:{icon:"⚖️",word:"charge sheet"} }; function isRepeatable(si){return !!REPEAT_META[STAGES[si].id];} function isRemarksStage(si){return STAGES[si] && STAGES[si].type==="remarks";} function stageIdOf(si){return STAGES[si].id;} function siOfStageId(id){return STAGES.findIndex(s=>s.id===id);} function instItemId(si,ci,ii,inst){return inst>1?itemId(si,ci,ii)+"#"+inst:itemId(si,ci,ii);} function stageItemKeys(si,count){const keys=[];const n=isRepeatable(si)?Math.max(1,count||1):1;STAGES[si].sections.forEach((sc,ci)=>sc.items.forEach((it,ii)=>{for(let s=1;s<=n;s++)keys.push(instItemId(si,ci,ii,s));}));return keys;} function instanceKeys(si,inst){const a=[];STAGES[si].sections.forEach((sc,ci)=>sc.items.forEach((it,ii)=>a.push(instItemId(si,ci,ii,inst))));return a;} function countFromChecks(checks,si){let n=1;if(checks){const pre="s"+si+"_";Object.keys(checks).forEach(k=>{if(k.indexOf(pre)===0){const m=k.match(/#(\d+)$/);if(m){const v=+m[1];if(v>n)n=v;}}});}return n;} function countForCase(c,si){if(!isRepeatable(si))return 1;const id=stageIdOf(si);let n=1;if(c&&c.instanceCounts&&c.instanceCounts[id])n=c.instanceCounts[id];else if(id==="suspect"&&c&&c.suspectCount)n=c.suspectCount;return Math.max(1,n,countFromChecks(c&&c.checks,si));} function allItemKeysForCase(c){let a=[];STAGES.forEach((st,si)=>{a=a.concat(stageItemKeys(si,countForCase(c,si)));});return a;} function draftCountFor(si){return isRepeatable(si)?Math.max(1,draftCounts[stageIdOf(si)]||1):1;} function allItemKeysDraft(){let a=[];STAGES.forEach((st,si)=>{a=a.concat(stageItemKeys(si,draftCountFor(si)));});return a;} /* ===================== STORAGE & API ===================== */ const CASE_KEY="gp_ccoe_cases_v4"; const USER_KEY="gp_ccoe_users_v1"; const SESS_KEY="gp_ccoe_session_v1"; const store=(()=>{let mem={},ok=false; const server = window.__CCOE_SERVER_DATA || {}; try{localStorage.setItem("__t","1");localStorage.removeItem("__t");ok=true;}catch(e){ok=false;} function fromServer(k){return (server && server[k] !== undefined && server[k] !== null && server[k] !== "") ? server[k] : null;} function saveToServer(k,v){ if(k===SESS_KEY||k===USER_KEY)return; try{fetch("ccoe_data.php",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:k,value:v})}).catch(()=>{});}catch(e){} } return{persistent:!server.__db_error, get(k){ const sv=fromServer(k); if(sv!==null) return sv; try{return ok?localStorage.getItem(k):(mem[k]??null);}catch(e){return mem[k]??null;} }, set(k,v){try{ok?localStorage.setItem(k,v):mem[k]=v;}catch(e){mem[k]=v;} saveToServer(k,v);} }; })(); let USERS=[], SESSION=null; async function authApi(action,data={}){ const res=await fetch("auth.php?action="+encodeURIComponent(action),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(data)}); let out={};try{out=await res.json();}catch(e){} if(!res.ok||!out.success)throw new Error(out.error||"Request failed"); return out; } function applyUsers(list){if(Array.isArray(list)&&list.length){USERS=list;}} function loadUsers(){applyUsers(window.__CCOE_SERVER_USERS||[]);} function togglePw(inputId, btnId){ const inp=document.getElementById(inputId),btn=document.getElementById(btnId); const show=inp.type==="password"; inp.type=show?"text":"password"; btn.textContent=show?"🙈":"👁"; btn.classList.toggle("on",show); } /* ===================== FORGOT PASSWORD ===================== */ function showForgotView(step){ document.getElementById("loginView").classList.remove("show"); document.getElementById("appView").classList.remove("show"); document.getElementById("forgotView").classList.add("show"); document.getElementById("fgStep1").style.display = step===1 ? "block" : "none"; document.getElementById("fgStep2").style.display = step===2 ? "block" : "none"; document.getElementById("fgStep3").style.display = step===3 ? "block" : "none"; if(step===1){ const lgUser = document.getElementById("lgUser").value; if(lgUser) document.getElementById("fgUser").value = lgUser; document.getElementById("fgErr1").textContent = ""; } } function backToLogin(){ document.getElementById("forgotView").classList.remove("show"); showLogin(); } async function sendOtp(){ const un = document.getElementById("fgUser").value.trim().toLowerCase(); if(!un){ document.getElementById("fgErr1").textContent = "Please enter your username."; return; } const btn = document.getElementById("btnSendOtp"); btn.disabled = true; btn.textContent = "Sending..."; try { await authApi("send_otp", { username: un }); document.getElementById("fgUserDisplay").textContent = un; document.getElementById("fgErr2").textContent = ""; showForgotView(2); } catch(e) { document.getElementById("fgErr1").textContent = e.message || "Could not send OTP."; } finally { btn.disabled = false; btn.textContent = "Send OTP"; } } async function verifyOtp(){ const otp = document.getElementById("fgOtp").value.trim(); if(!otp){ document.getElementById("fgErr2").textContent = "Please enter the OTP."; return; } const btn = document.getElementById("btnVerifyOtp"); btn.disabled = true; btn.textContent = "Verifying..."; try { await authApi("verify_otp", { otp }); document.getElementById("fgErr3").textContent = ""; document.getElementById("fgOk3").style.display = "none"; document.getElementById("btnResetPw").style.display = "block"; document.getElementById("fgPass1").value = ""; showForgotView(3); } catch(e) { document.getElementById("fgErr2").textContent = e.message || "Invalid OTP."; } finally { btn.disabled = false; btn.textContent = "Verify Code"; } } async function resetPasswordOtp(){ const pw = document.getElementById("fgPass1").value; if(pw.length < 4){ document.getElementById("fgErr3").textContent = "Password must be at least 4 characters."; return; } const btn = document.getElementById("btnResetPw"); btn.disabled = true; btn.textContent = "Updating..."; try { await authApi("reset_password_with_otp", { password: pw }); document.getElementById("fgErr3").textContent = ""; document.getElementById("fgOk3").style.display = "block"; btn.style.display = "none"; setTimeout(() => { backToLogin(); }, 2500); } catch(e) { document.getElementById("fgErr3").textContent = e.message || "Could not reset password."; btn.disabled = false; btn.textContent = "Reset Password"; } } /* ===================== LOGIN ===================== */ async function doLogin(){ const un=document.getElementById("lgUser").value.trim().toLowerCase(); const pw=document.getElementById("lgPass").value; const btn=document.querySelector('#loginView .btn.gold'); const oldTxt=btn?btn.textContent:""; try{ if(btn){btn.disabled=true;btn.textContent="Signing in...";} const out=await authApi("login",{username:un,password:pw}); applyUsers(out.users); SESSION=out.user; document.getElementById("lgErr").textContent=""; document.getElementById("lgPass").value=""; showApp(); }catch(e){document.getElementById("lgErr").textContent=e.message||"Invalid username or password.";} finally{if(btn){btn.disabled=false;btn.textContent=oldTxt||"Login";}} } async function logout(){try{await authApi("logout",{});}catch(e){} SESSION=null;showLogin();} function showLogin(){document.getElementById("appView").classList.remove("show");document.getElementById("forgotView").classList.remove("show");document.getElementById("loginView").classList.add("show");document.body.style.overflow="";document.getElementById("lgUser").focus&&document.getElementById("lgUser").focus();} function showApp(){ if(!SESSION){showLogin();return;} document.getElementById("loginView").classList.remove("show"); document.getElementById("forgotView").classList.remove("show"); document.getElementById("appView").classList.add("show"); document.getElementById("uName").textContent=SESSION.name+(SESSION.zone?(" · "+SESSION.zone):""); const role=SESSION.role; const rb=document.getElementById("uRole");rb.textContent=role.toUpperCase();rb.className="role-badge "+role; const isAdminU=role==="admin", isReviewerU=(role==="sp"||role==="ig"); document.querySelectorAll(".admin-only").forEach(el=>el.style.display=isAdminU?"":"none"); document.querySelectorAll(".admin-sp").forEach(el=>el.style.display=(isAdminU||isReviewerU)?"":"none"); document.querySelectorAll(".no-sp").forEach(el=>el.style.display=isReviewerU?"none":""); render(); } function isAdmin(){return SESSION&&SESSION.role==="admin";} function isSP(){return SESSION&&SESSION.role==="sp";} function isIG(){return SESSION&&SESSION.role==="ig";} function isReviewer(){return isSP()||isIG();} function canEditRemarks(){return isAdmin()||isSP()||isIG();} async function changeMyPassword(){ const np=prompt("Enter a new password for "+SESSION.name+":"); if(np===null)return; if(np.length<4){toast("Password too short (min 4)","warn");return;} try{const out=await authApi("set_password",{username:SESSION.username,password:np});applyUsers(out.users);toast("Password updated","ok");} catch(e){toast(e.message||"Password update failed","warn");} } /* ===================== CASES ===================== */ let cases=[]; let sort={field:"daysFIR",dir:"desc"}; let editing=null, draftChecks={}, openStages={}, draftCounts={}, draftLabels={}, draftDates={}, draftCS={}, draftLimit={}, draftBail={}, draftRemarks=[], draftRemarksDone=false; function _mk(ids){const c={};ids.forEach(k=>c[k]=true);return c;} const DEFAULT_CASES=[ { id:"seed_fir1724", firNo:"17/2024", crimeType:"Digital Arrest", piName:"PI P D MAKWANA", zone:"Gandhinagar", firDate:"2024-09-08", lastUpdate:"2024-12-10", lossAmount:39922578, frozenAmount:4600000, recoveryAmount:0, notes:"FedEx courier / Digital Arrest fraud.", checks:_mk(["s0_0_0","s0_0_1","s0_0_2","s0_0_3","s0_0_4","s0_0_5","s0_0_6","s0_1_0","s0_1_1","s0_1_2","s0_1_3", "s1_0_0","s1_0_1","s1_0_2","s1_0_3","s1_0_4","s1_0_5","s1_0_6","s1_0_7","s1_0_9","s1_1_0","s1_1_1","s1_1_2","s1_1_3", "s2_0_0","s2_0_1","s2_0_3","s2_0_4","s2_0_5","s2_0_6","s2_1_0","s2_1_1","s2_1_3","s2_2_0","s2_2_1","s2_2_2","s2_2_3","s2_3_0","s2_3_1","s2_3_2","s2_3_3", "s3_0_0","s3_0_1","s3_0_2","s3_0_3","s3_0_4","s3_1_1","s3_1_3","s4_0_0","s4_0_1","s4_0_2","s4_0_3","s4_0_5"]) }, { id:"seed_fir1025", firNo:"10/2025", crimeType:"Investment Scam", piName:"PI G B DODIA", zone:"Vadodara", firDate:"2025-02-14", lastUpdate:"2025-04-17", lossAmount:469590010, frozenAmount:155000000, recoveryAmount:0, notes:"Investment fraud.", checks:_mk(["s0_0_0","s0_0_1","s0_0_2","s0_0_3","s0_0_4","s0_0_5","s0_0_6","s0_1_0","s0_1_1","s0_1_2","s0_1_3", "s1_0_0","s1_0_1","s1_0_2","s1_0_3","s1_0_4","s1_0_5","s1_0_6","s1_0_7","s1_0_9","s1_1_0","s1_1_1","s1_1_2","s1_1_3", "s2_0_0","s2_0_1","s2_0_2","s2_0_3","s2_0_4","s2_0_5","s2_0_6","s2_1_0","s2_1_1","s2_1_2","s2_1_3","s2_2_0","s2_2_1","s2_2_2","s2_2_3","s2_3_0","s2_3_1","s2_3_2","s2_3_3", "s3_0_0","s3_0_1","s3_0_2","s3_0_3","s3_0_4","s3_1_3","s4_0_0","s4_0_1","s4_0_3"]) }, ]; const SEEDED_KEY="gp_ccoe_seeded_v1"; function loadCases(){ const raw=store.get(CASE_KEY); cases = raw ? (function(){try{return JSON.parse(raw);}catch(e){return [];}})() : []; let seeded=[];const sr=store.get(SEEDED_KEY);if(sr){try{seeded=JSON.parse(sr);}catch(e){seeded=[];}} let changed=false; DEFAULT_CASES.forEach(dc=>{ if(!seeded.includes(dc.id)){ if(!cases.some(c=>c.id===dc.id)) cases.push(JSON.parse(JSON.stringify(dc))); seeded.push(dc.id); changed=true; } }); if(changed){store.set(SEEDED_KEY,JSON.stringify(seeded));saveAll();} } function saveAll(){store.set(CASE_KEY,JSON.stringify(cases));} const EVID_MIGRATION_KEY="gp_ccoe_evid_moved_v1"; function migrateEvidenceToComplaint(){ if(store.get(EVID_MIGRATION_KEY)){return;} let touched=false; cases.forEach(c=>{ if(!c.checks)return; const nc={}; Object.keys(c.checks).forEach(k=>{ const m=/^s2_(\d+)_(\d+)(#\d+)?$/.exec(k); if(!m){nc[k]=c.checks[k];return;} const sec=+m[1], item=m[2], inst=m[3]||""; if(sec===0){ if(inst)return; nc["s0_2_"+item]=c.checks[k]; }else{ nc["s2_"+(sec-1)+"_"+item+inst]=c.checks[k]; } touched=true; }); c.checks=nc; }); if(touched)saveAll(); store.set(EVID_MIGRATION_KEY,"1"); } const CS_SHIFT_MIGRATION_KEY="gp_ccoe_cs_shift_v1"; function migrateChargesheetShift(){ if(store.get(CS_SHIFT_MIGRATION_KEY)){return;} let touched=false; cases.forEach(c=>{ if(!c.checks)return; const nc={}; Object.keys(c.checks).forEach(k=>{ const m=/^s5_(\d+)_(\d+)(#\d+)?$/.exec(k); if(!m){nc[k]=c.checks[k];return;} nc["s6_"+m[1]+"_"+m[2]+(m[3]||"")]=c.checks[k]; touched=true; }); c.checks=nc; }); if(touched)saveAll(); store.set(CS_SHIFT_MIGRATION_KEY,"1"); } function uid(){return "C"+Date.now().toString(36)+Math.random().toString(36).slice(2,6);} function visibleCases(){return (isAdmin()||isReviewer())?cases:cases.filter(c=>c.piName===SESSION.name&&c.zone===SESSION.zone);} /* ===================== METRICS ===================== */ function stageProgress(c,si){const keys=stageItemKeys(si,countForCase(c,si));if(!keys.length)return 0;let d=0;keys.forEach(k=>{if(c.checks&&c.checks[k])d++;});return Math.round(d/keys.length*100);} function overallProgress(c){const keys=allItemKeysForCase(c);let d=0;keys.forEach(k=>{if(c.checks&&c.checks[k])d++;});return keys.length?Math.round(d/keys.length*100):0;} function currentStageIdx(c){let idx=0;STAGES.forEach((st,si)=>{if(stageProgress(c,si)>0)idx=si;});return idx;} function isClosed(c){const s=STAGES.length-1,sec=STAGES[s].sections.length-1,it=STAGES[s].sections[sec].items;return !!(c.checks&&c.checks[itemId(s,sec,it.length-1)]);} function chargeSheetFiled(c){const s=STAGES.length-1,sec=STAGES[s].sections.length-1;return !!(c.checks&&c.checks[itemId(s,sec,0)]);} function daysBetween(d){if(!d)return null;return Math.floor((new Date()-new Date(d))/86400000);} function statusOf(c){ if(isClosed(c))return {key:"closed",rank:0,cls:"muted",label:"Closed"}; if(chargeSheetFiled(c))return {key:"filed",rank:1,cls:"green",label:"Charge sheet filed"}; const dU=daysBetween(c.lastUpdate),dF=daysBetween(c.firDate); if((dU!=null&&dU>30)||(dF!=null&&dF>90))return {key:"red",rank:4,cls:"red",label:"Delayed"}; if(dU!=null&&dU>14)return {key:"amber",rank:3,cls:"amber",label:"Needs attention"}; return {key:"green",rank:2,cls:"green",label:"On track"}; } function esc(s){return String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));} function hexa(h){return h+"22";} /* ===================== ARREST 90-DAY DUE ALERT ===================== */ const ARREST_LIMIT_DAYS=90, ARREST_ALERT_WINDOW=10; function arrestUrgency(p){if(p<0)return "overdue";if(p<=3)return "red";if(p<=7)return "orange";if(p<=10)return "yellow";return "safe";} function fmtDMY(iso){if(!iso)return "—";const d=new Date(iso);if(isNaN(d))return esc(iso);const z=n=>String(n).padStart(2,"0");return z(d.getDate())+"-"+z(d.getMonth()+1)+"-"+d.getFullYear();} function arrestInstanceCount(c){ let n=countForCase(c,siOfStageId("arrest")); [c.instanceDates,c.instanceLabels,c.instanceCS].forEach(o=>{if(o)Object.keys(o).forEach(k=>{const m=/^arrest#(\d+)$/.exec(k);if(m)n=Math.max(n,+m[1]);});}); return Math.max(1,n); } function arrestDueList(){ const out=[]; visibleCases().forEach(c=>{ const count=arrestInstanceCount(c); for(let inst=1;inst<=count;inst++){ const key="arrest#"+inst; if(c.instanceCS&&c.instanceCS[key])continue; if(c.instanceBail&&c.instanceBail[key])continue; const ad=c.instanceDates&&c.instanceDates[key]; if(!ad)continue; const since=daysBetween(ad); if(since===null)continue; const limit=parseInt(c.instanceLimit&&c.instanceLimit[key],10)||ARREST_LIMIT_DAYS; const pending=limit-since; const lbl=c.instanceLabels||{}; const suspect=(lbl["arrest#"+inst]||lbl["suspect#"+inst]||"").trim(); out.push({caseId:c.id,firNo:c.firNo,piName:c.piName,zone:c.zone,suspect,arrestDate:ad,pending,limit}); } }); out.sort((a,b)=>a.pending-b.pending||String(a.firNo).localeCompare(String(b.firNo))); return out; } function arrestAlertCardHTML(){ const all=arrestDueList(), total=all.length; if(!total){ return `
🛡️ Arrest Charge-Sheet Watch0 pending
✓ No arrested suspects with a pending charge sheet.
`; } const critical=all.filter(d=>d.pending<=ARREST_ALERT_WINDOW); const nc=critical.length; const sub=`${total} suspect${total>1?"s":""} pending overall · soonest first`; if(!nc){ const soon=all[0].pending; return `
⏳ Arrest Charge-Sheet Tracker0 due
${sub}
✓ None due within 10 days — soonest is ${soon} day${soon===1?"":"s"} away.
`; } const rows=critical.map(d=>{ const u=arrestUrgency(d.pending); const pend=d.pending<0?`${Math.abs(d.pending)}d over`:`${d.pending}d`; return ` ${esc(d.firNo||"—")} ${esc(d.piName||"—")} ${esc(d.suspect||"—")} ${fmtDMY(d.arrestDate)} ${pend}/${d.limit} `; }).join(""); return `
🚨 Arrest Cases Due Within 10 Days${nc} due
${sub}
${rows}
Case No.PI NameSuspectArrest DatePending
`; } /* ===================== RENDER ===================== */ function renderSummary(){ const list=visibleCases(); const active=list.filter(c=>!isClosed(c)); const red=list.filter(c=>statusOf(c).key==="red").length; const amber=list.filter(c=>statusOf(c).key==="amber").length; const avg=list.length?Math.round(list.reduce((a,c)=>a+overallProgress(c),0)/list.length):0; const dist=STAGES.map(()=>0);active.forEach(c=>dist[currentStageIdx(c)]++); const distBar=STAGES.map((st,i)=>``).join(""); const legend=STAGES.map((st,i)=>`
${st.name} · ${dist[i]}
`).join(""); const zdist={};ZONES.forEach(z=>zdist[z]=0);list.forEach(c=>{if(c.zone&&zdist[c.zone]!=null)zdist[c.zone]++;}); const zBar=ZONES.map(z=>``).join(""); const zLeg=ZONES.map(z=>`
${z} · ${zdist[z]}
`).join(""); const zoneCard=(isAdmin()||isReviewer())?`
Cases by Zone
${zBar}
${zLeg}
`:""; document.getElementById("summary").innerHTML=`
${isAdmin()?"Total Cases":"My Cases"}
${list.length}
${active.length} active · ${list.length-active.length} closed
Delayed
${red}
No update >30d or >90d since FIR
Needs Attention
${amber}
No update in 14+ days
Avg. Progress
${avg}%
across these cases
Active Cases by Stage
${distBar}
${legend}
${zoneCard}${arrestAlertCardHTML()}`; } function passesFilter(c){ const q=document.getElementById("search").value.trim().toLowerCase(); const fZone=document.getElementById("fZone").value, fStage=document.getElementById("fStage").value; const fType=document.getElementById("fType").value, fStatus=document.getElementById("fStatus").value; if(q){const hay=((c.firNo||"")+" "+(c.piName||"")).toLowerCase();if(!hay.includes(q))return false;} if(fZone&&c.zone!==fZone)return false; if(fStage!==""&&String(currentStageIdx(c))!==fStage)return false; if(fType&&c.crimeType!==fType)return false; if(fStatus&&statusOf(c).key!==fStatus)return false; return true; } function sortVal(c,f){switch(f){ case "firNo":return c.firNo||"";case "crimeType":return c.crimeType||"";case "piName":return c.piName||""; case "zone":return c.zone||"";case "stage":return currentStageIdx(c);case "progress":return overallProgress(c); case "daysFIR":return daysBetween(c.firDate)??-1;case "statusRank":return statusOf(c).rank;default:return 0;}} function sortBy(f){if(sort.field===f)sort.dir=sort.dir==="asc"?"desc":"asc";else{sort.field=f;sort.dir="desc";}render();} function renderTable(){ let rows=visibleCases().filter(passesFilter); rows.sort((a,b)=>{let va=sortVal(a,sort.field),vb=sortVal(b,sort.field),r;if(typeof va==="string")r=va.localeCompare(vb);else r=va-vb;return sort.dir==="asc"?r:-r;}); const tb=document.getElementById("tbody"); if(!rows.length){tb.innerHTML=`

No cases yet

Click + New Case to register the first investigation.
`;return;} tb.innerHTML=rows.map(c=>{ const si=currentStageIdx(c),prog=overallProgress(c),st=statusOf(c); const dF=daysBetween(c.firDate),dU=daysBetween(c.lastUpdate); const dFc=(dF!=null&&dF>90)?"var(--red)":(dF!=null&&dF>60)?"var(--amber)":"var(--text)"; const zc=ZONE_COLORS[c.zone]||"#8b99b5"; const zoneCell=c.zone?`${esc(c.zone)}`:`—`; return ` ${esc(c.firNo)} ${esc(c.crimeType)} ${esc(c.piName||"—")} ${zoneCell} ${si+1}· ${STAGES[si].name}
${prog}%
${dF??"—"}d
upd ${dU??"—"}d ago
${st.cls==="red"?"⚠ ":""}${st.label} `; }).join(""); } function render(){if(!SESSION)return;renderSummary();renderTable();refreshNotif();} /* ===================== NOTIFICATIONS (remarks) ===================== */ function casesWithRemarks(){return visibleCases().filter(c=>c.remarks&&c.remarks.length&&!c.remarksDone);} function refreshNotif(){ const wrap=document.getElementById("notifWrap");if(!wrap)return; const list=casesWithRemarks(); const panel=document.getElementById("notifPanel"); if(!list.length){wrap.style.display="none";if(panel)panel.classList.remove("open");return;} wrap.style.display=""; document.getElementById("notifCount").textContent=list.length; document.getElementById("notifList").innerHTML=list.map(c=>{ const last=c.remarks[c.remarks.length-1],txt=(last&&last.text)||""; return `
${esc(c.firNo||"—")}${c.remarks.length} remark${c.remarks.length>1?"s":""}
${esc(txt.slice(0,90))}${txt.length>90?"…":""}
— ${esc((last&&last.by)||"SP")} · ${esc(c.piName||"")}
`; }).join(""); } function toggleNotif(e){if(e)e.stopPropagation();const p=document.getElementById("notifPanel");if(p)p.classList.toggle("open");} function openFromNotif(id){const p=document.getElementById("notifPanel");if(p)p.classList.remove("open");openCase(id);} function initFilters(){ const fz=document.getElementById("fZone");ZONES.forEach(z=>fz.insertAdjacentHTML("beforeend",``)); const fs=document.getElementById("fStage");STAGES.forEach((s,i)=>{if(s.type==="remarks")return;fs.insertAdjacentHTML("beforeend",``);}); const ft=document.getElementById("fType");CRIME_TYPES.forEach(t=>ft.insertAdjacentHTML("beforeend",``)); } /* ===================== DRAWER ===================== */ function blankCase(){return{id:uid(),firNo:"",crimeType:CRIME_TYPES[0],piName:isAdmin()?"":SESSION.name,zone:isAdmin()?"":SESSION.zone, firDate:new Date().toISOString().slice(0,10),lastUpdate:new Date().toISOString().slice(0,10),lossAmount:0,frozenAmount:0,recoveryAmount:0,instanceCounts:{},instanceLabels:{},notes:"",checks:{},remarks:[]};} function piOptions(selPi,selZone){ const ioUsers=USERS.filter(u=>u.role==="io").map(u=>({pi:u.name,zone:u.zone})); let html=``,matched=false; if(selPi&&!ioUsers.some(r=>r.pi===selPi&&(!selZone||r.zone===selZone))) html+=``; ZONES.forEach(z=>{const m=ioUsers.filter(r=>r.zone===z);if(!m.length)return;html+=``; m.forEach(r=>{const v=`${r.pi}|||${r.zone}`;const isM=!matched&&r.pi===selPi&&(!selZone||r.zone===selZone);if(isM)matched=true; html+=``;}); html+=``;}); return html; } function zoneOptions(sel){let h=``;ZONES.forEach(z=>h+=``);return h;} function openCase(id){ let c; if(id){c=cases.find(x=>x.id===id);if(!c)return; if(!isAdmin()&&!isReviewer()&&(c.piName!==SESSION.name||c.zone!==SESSION.zone)){toast("You can only open your own cases","warn");return;} editing=id; const canDelete=isAdmin()||(c.piName===SESSION.name&&c.zone===SESSION.zone); document.getElementById("delBtn").style.display=canDelete?"inline-flex":"none";} else{c=blankCase();editing=null;document.getElementById("delBtn").style.display="none";} draftChecks=Object.assign({},c.checks||{});draftLabels=Object.assign({},c.instanceLabels||{});draftDates=Object.assign({},c.instanceDates||{});draftCS=Object.assign({},c.instanceCS||{});draftLimit=Object.assign({},c.instanceLimit||{});draftBail=Object.assign({},c.instanceBail||{});draftRemarks=JSON.parse(JSON.stringify(c.remarks||[]));draftRemarksDone=!!c.remarksDone;draftCounts={};STAGES.forEach((st,si)=>{if(isRepeatable(si))draftCounts[st.id]=countForCase(c,si);});openStages={};openStages[currentStageIdx(c)]=true;if(canEditRemarks())openStages[siOfStageId("remarks")]=true; buildDrawer(c); document.getElementById("dTitle").textContent=id?c.firNo||"Case":"New Case"; document.getElementById("dSubtitle").textContent=id?`${c.crimeType} · ${c.zone||"No zone"} · ${c.piName||"—"}`:"Enter case details and track the investigation"; document.getElementById("overlay").classList.add("open");document.getElementById("drawer").classList.add("open");document.body.style.overflow="hidden"; } function closeCase(){document.getElementById("overlay").classList.remove("open");document.getElementById("drawer").classList.remove("open");document.body.style.overflow="hidden";document.body.style.overflow="";editing=null;} function buildDrawer(c){ const prog=overallProgressDraft(),si=currentStageIdxDraft(); const st=statusOf({...c,checks:draftChecks});const dF=daysBetween(c.firDate); const R=42,CIRC=2*Math.PI*R,off=CIRC*(1-prog/100); const piField=isAdmin() ? `
` : `
`; const zoneField=isAdmin() ? `
` : `
`; let html=`
${prog}%
Current stage · ${si+1} — ${STAGES[si].name}
${st.cls==="red"?"⚠ ":""}${st.label}${dF??"—"} days since FIR
${isReviewer()?"Review-only view. Scroll to the Remarks stage to add or edit supervisory remarks.":"Tick checklist items below — stage and overall progress update automatically."}
Case Tracking Fields
${piField} ${zoneField}
Case Notes / Summary
Investigation Workflow
`; const body=document.getElementById("drawerBody"); body.className="drawer-body"+(isReviewer()?" ro":""); body.innerHTML=html;renderStages(); } function onPIChange(sel){const v=sel.value,opt=sel.options[sel.selectedIndex];let z=opt?opt.getAttribute("data-zone"):"";if(!z&&v.includes("|||"))z=v.split("|||")[1];if(z){const zs=document.getElementById("f_zone");if(zs)zs.value=z;}} function sectionsHTML(si,inst){return STAGES[si].sections.map((sc,ci)=>`

${esc(sc.title)}

${sc.items.map((it,ii)=>{const id=instItemId(si,ci,ii,inst);const on=!!draftChecks[id];return ``;}).join("")}
`).join("");} function instanceBlock(si,inst,count){ const id=stageIdOf(si),meta=REPEAT_META[id]; const rm=count>1?``:""; const label=esc(draftLabels[id+"#"+inst]||""); const nameBox=``; const dateBox=id==="arrest"?instDateBox(id,inst):""; return `
${meta.icon} ${STAGES[si].name} ${inst}${rm}
${nameBox}${dateBox}${sectionsHTML(si,inst)}
`; } function stageBodyHTML(si){ if(isRemarksStage(si))return remarksPanelHTML(); if(!isRepeatable(si))return sectionsHTML(si,1); const id=stageIdOf(si),meta=REPEAT_META[id],count=draftCountFor(si); let h=""; for(let s=1;s<=count;s++) h+=instanceBlock(si,s,count); h+=``; return h; } /* ---------- Remarks ---------- */ function fmtRemarkTime(iso){if(!iso)return "";const d=new Date(iso);if(isNaN(d))return "";const z=n=>String(n).padStart(2,"0"); return z(d.getDate())+"-"+z(d.getMonth()+1)+"-"+d.getFullYear()+" "+z(d.getHours())+":"+z(d.getMinutes());} function remarksPanelHTML(){ const canEdit=canEditRemarks(); const items=(draftRemarks||[]).map(r=>{ const actions=canEdit?`
`:""; return `
🎖 ${esc(r.by||"SP")}${esc(fmtRemarkTime(r.ts))}${actions}
${esc(r.text||"").replace(/\n/g,"
")}
`; }).join(""); const list=(draftRemarks&&draftRemarks.length) ? `
${items}
` : `
${canEdit?"No remarks yet — add the first supervisory remark below.":"No supervisory remarks have been added for this case yet."}
`; const done=!!draftRemarksDone; let doneBar=""; if(draftRemarks&&draftRemarks.length){ doneBar=`
✓ Done
`; } const footer=canEdit ? `
` : `
🔒 Read-only. Only the SP officers, the IG and the Administrator can add or edit remarks.
`; return `
${doneBar}${list}${footer}
`; } function reRenderRemarks(){const si=siOfStageId("remarks");openStages[si]=true;renderStages();refreshNotif();} function toggleRemarksDone(on){ draftRemarksDone=!!on; if(editing){const c=cases.find(x=>x.id===editing);if(c){c.remarksDone=draftRemarksDone;saveAll();}} const bar=document.getElementById("rmkDoneBar");if(bar)bar.classList.toggle("is-done",draftRemarksDone); refreshNotif(); toast(draftRemarksDone?"Marked done — cleared from notifications":"Reopened — back in notifications",draftRemarksDone?"ok":"warn"); } function addRemark(){ if(!canEditRemarks()){toast("You are not allowed to add remarks","warn");return;} const ta=document.getElementById("rmkInput"),t=ta?ta.value.trim():""; if(!t){toast("Type a remark first","warn");if(ta)ta.focus();return;} draftRemarks.push({id:"r"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),text:t,by:SESSION.name,ts:new Date().toISOString()}); draftRemarksDone=false; reRenderRemarks(); toast("Remark added — click Save case to store it","ok"); } function editRemark(id){ if(!canEditRemarks())return; const r=(draftRemarks||[]).find(x=>x.id===id);if(!r)return; const nv=prompt("Edit remark:",r.text);if(nv===null)return; if(!nv.trim()){toast("Remark cannot be empty","warn");return;} r.text=nv.trim();r.ts=new Date().toISOString();r.by=SESSION.name; reRenderRemarks();toast("Remark updated — click Save case to store","ok"); } function deleteRemark(id){ if(!canEditRemarks())return; if(!confirm("Delete this remark?"))return; draftRemarks=(draftRemarks||[]).filter(x=>x.id!==id); reRenderRemarks();toast("Remark removed — click Save case to store","warn"); } function renderStages(){ document.getElementById("stageList").innerHTML=STAGES.map((st,si)=>{ const open=openStages[si]?"open":""; const body=stageBodyHTML(si); if(isRemarksStage(si)){ const n=(draftRemarks||[]).length; const badge=n?`${n} remark${n>1?"s":""}`:""; const numTxt=n?"💬":(si+1); return `
${numTxt}
${st.name}${badge}
▶
${body}
`; } const p=stageProgressDraft(si),cls=p===100?"done":(p>0?"active":""); let nm=st.name; if(isRepeatable(si)){const cnt=draftCountFor(si);if(cnt>1)nm+=` ×${cnt}`;} return `
${p===100?"✓":si+1}
${nm}
${p}%
▶
${body}
`; }).join(""); } function syncHero(){ const prog=overallProgressDraft(),si=currentStageIdxDraft(); const hp=document.getElementById("heroPct");if(hp)hp.textContent=prog+"%"; const hs=document.getElementById("heroStage");if(hs)hs.textContent=(si+1)+" — "+STAGES[si].name; const R=42,CIRC=2*Math.PI*R,rc=document.querySelector('.ring circle:nth-child(2)');if(rc)rc.setAttribute("stroke-dashoffset",CIRC*(1-prog/100)); } function setInstLabel(stageId,inst,val){draftLabels[stageId+"#"+inst]=val;} function todayISO(){const d=new Date();return new Date(d.getTime()-d.getTimezoneOffset()*60000).toISOString().slice(0,10);} function arrestDayInfo(days,limit){ limit=limit||ARREST_LIMIT_DAYS; if(days===null||days===undefined||isNaN(days))return {txt:"",cls:"muted"}; if(days<0)return {txt:"⚠ Future date",cls:"warn"}; const pending=limit-days; if(pending<0)return {txt:Math.abs(pending)+" days overdue · "+limit+"-day limit",cls:"warn"}; if(pending===0)return {txt:"Last day · "+limit+"-day limit today",cls:"warn"}; if(pending===1)return {txt:"1 day pending · "+limit+"-day limit",cls:"warn"}; return {txt:pending+" days pending · "+limit+"-day limit",cls:(pending<=10?"warn":"ok")}; } function draftLimitFor(key){return parseInt(draftLimit[key],10)||ARREST_LIMIT_DAYS;} function arrInfoFor(key){ if(draftCS[key])return {txt:"✓ Charge sheet filed",cls:"ok"}; if(draftBail[key])return {txt:"⚖️ Bail granted — not pending",cls:"ok"}; return arrestDayInfo(draftDates[key]?daysBetween(draftDates[key]):null,draftLimitFor(key)); } function instDateBox(stageId,inst){ const key=stageId+"#"+inst,val=esc(draftDates[key]||""); const csFiled=!!draftCS[key], lim=draftLimitFor(key), bail=!!draftBail[key]; const info=arrInfoFor(key); return `
${info.txt}
`; } function refreshArrDays(stageId,inst){ const key=stageId+"#"+inst, el=document.getElementById("arrDays_"+stageId+"_"+inst); if(!el)return; const info=arrInfoFor(key); el.textContent=info.txt;el.className="inst-days "+info.cls; } function setInstBail(stageId,inst,val){ const key=stageId+"#"+inst; if(val==="yes")draftBail[key]=true;else delete draftBail[key]; const sel=document.querySelector(`select.inst-bail[onchange="setInstBail('${stageId}',${inst},this.value)"]`); if(sel)sel.classList.toggle("yes",val==="yes"); refreshArrDays(stageId,inst); } function setInstLimit(stageId,inst,val){ const key=stageId+"#"+inst, n=parseInt(val,10); if(n===60||n===90)draftLimit[key]=n;else delete draftLimit[key]; refreshArrDays(stageId,inst); } function setInstCS(stageId,inst,checked){ const key=stageId+"#"+inst; if(checked)draftCS[key]=true;else delete draftCS[key]; refreshArrDays(stageId,inst); } function setInstDate(stageId,inst,val){ const key=stageId+"#"+inst; if(val)draftDates[key]=val;else delete draftDates[key]; refreshArrDays(stageId,inst); } function titleCase(s){return s.replace(/^\w/,c=>c.toUpperCase());} function addInstance(stageId){const si=siOfStageId(stageId);draftCounts[stageId]=draftCountFor(si)+1;openStages[si]=true;renderStages();syncHero();toast(titleCase(REPEAT_META[stageId].word)+" "+draftCounts[stageId]+" added","ok");} function removeInstance(stageId,n){ const si=siOfStageId(stageId),count=draftCountFor(si); if(count<=1||n<1||n>count)return; if(!confirm("Remove "+REPEAT_META[stageId].word+" "+n+" and its checklist?"))return; instanceKeys(si,n).forEach(k=>{delete draftChecks[k];}); delete draftLabels[stageId+"#"+n]; delete draftDates[stageId+"#"+n]; delete draftCS[stageId+"#"+n]; delete draftLimit[stageId+"#"+n]; delete draftBail[stageId+"#"+n]; for(let s=n+1;s<=count;s++){ STAGES[si].sections.forEach((sc,ci)=>sc.items.forEach((it,ii)=>{ const from=instItemId(si,ci,ii,s),to=instItemId(si,ci,ii,s-1); if(draftChecks[from])draftChecks[to]=draftChecks[from];else delete draftChecks[to]; delete draftChecks[from];})); const lf=stageId+"#"+s,lt=stageId+"#"+(s-1); if(draftLabels[lf]!==undefined)draftLabels[lt]=draftLabels[lf];else delete draftLabels[lt]; delete draftLabels[lf]; if(draftDates[lf]!==undefined)draftDates[lt]=draftDates[lf];else delete draftDates[lt]; delete draftDates[lf]; if(draftCS[lf]!==undefined)draftCS[lt]=draftCS[lf];else delete draftCS[lt]; delete draftCS[lf]; if(draftLimit[lf]!==undefined)draftLimit[lt]=draftLimit[lf];else delete draftLimit[lt]; delete draftLimit[lf]; if(draftBail[lf]!==undefined)draftBail[lt]=draftBail[lf];else delete draftBail[lt]; delete draftBail[lf]; } draftCounts[stageId]=count-1;openStages[si]=true;renderStages();syncHero();toast(titleCase(REPEAT_META[stageId].word)+" "+n+" removed","warn"); } function stageProgressDraft(si){const keys=stageItemKeys(si,draftCountFor(si));if(!keys.length)return 0;let d=0;keys.forEach(k=>{if(draftChecks[k])d++;});return Math.round(d/keys.length*100);} function overallProgressDraft(){const keys=allItemKeysDraft();let d=0;keys.forEach(k=>{if(draftChecks[k])d++;});return keys.length?Math.round(d/keys.length*100):0;} function currentStageIdxDraft(){let idx=0;STAGES.forEach((st,si)=>{if(stageProgressDraft(si)>0)idx=si;});return idx;} function toggleStage(si){openStages[si]=!openStages[si];document.querySelector(`.stage[data-si="${si}"]`).classList.toggle("open");} function toggleItem(id,on){ draftChecks[id]=on; const prog=overallProgressDraft(),si=currentStageIdxDraft(); document.getElementById("heroPct").textContent=prog+"%"; document.getElementById("heroStage").textContent=(si+1)+" — "+STAGES[si].name; const R=42,CIRC=2*Math.PI*R;document.querySelector('.ring circle:nth-child(2)').setAttribute("stroke-dashoffset",CIRC*(1-prog/100)); STAGES.forEach((st,i)=>{const el=document.querySelector(`.stage[data-si="${i}"]`);if(!el)return; if(isRemarksStage(i))return; const p=stageProgressDraft(i); const mi=el.querySelector(".mini-prog i");if(mi)mi.style.width=p+"%"; const mp=el.querySelector(".mini-prog .pct");if(mp)mp.textContent=p+"%"; const sn=el.querySelector(".stage-num");if(sn)sn.textContent=p===100?"✓":i+1; el.classList.toggle("done",p===100);el.classList.toggle("active",p>0&&p<100);}); const cb=document.querySelector(`input[onchange="toggleItem('${id}',this.checked)"]`);if(cb)cb.closest(".chk").classList.toggle("checked",on); } function saveCase(){ const g=id=>document.getElementById(id); const fir=g("f_firNo").value.trim(); if(!fir){toast("Enter an FIR No. before saving","warn");g("f_firNo").focus();return;} let piName,zone; if(isAdmin()){const raw=g("f_piName").value;piName=raw.includes("|||")?raw.split("|||")[0]:raw;zone=g("f_zone").value;} else if(isReviewer()){const orig=editing?cases.find(c=>c.id===editing):null;piName=orig?orig.piName:"";zone=orig?orig.zone:"";} else{piName=SESSION.name;zone=SESSION.zone;} const data={firNo:fir,crimeType:g("f_crimeType").value,piName,zone,firDate:g("f_firDate").value,lastUpdate:g("f_lastUpdate").value, notes:g("f_notes")?g("f_notes").value:"",instanceCounts:Object.assign({},draftCounts),instanceLabels:Object.assign({},draftLabels),instanceDates:Object.assign({},draftDates),instanceCS:Object.assign({},draftCS),instanceLimit:Object.assign({},draftLimit),instanceBail:Object.assign({},draftBail),suspectCount:(draftCounts.suspect||1),checks:Object.assign({},draftChecks),remarks:JSON.parse(JSON.stringify(draftRemarks||[])),remarksDone:!!draftRemarksDone}; if(editing){const i=cases.findIndex(c=>c.id===editing);cases[i]={...cases[i],...data};} else cases.push({id:uid(),...data}); saveAll();render();closeCase();toast("Case saved","ok"); } function deleteCase(){ if(!editing)return; const c=cases.find(x=>x.id===editing);if(!c)return; const canDelete=isAdmin()||(c.piName===SESSION.name&&c.zone===SESSION.zone); if(!canDelete){toast("You can only delete your own cases","warn");return;} const msg=`Delete case ${c.firNo||"(no FIR no.)"} — ${c.crimeType}?\n\nAssigned IO: ${c.piName||"—"}\nZone: ${c.zone||"—"}\n\nThis permanently removes the case and all its progress. This cannot be undone.`; if(!confirm(msg))return; cases=cases.filter(x=>x.id!==editing); saveAll();render();closeCase(); toast("Case deleted","warn"); } /* ===================== OFFICERS MODAL ===================== */ function openOfficers(){ if(!isAdmin())return; renderOfficers(); document.getElementById("offOverlay").classList.add("open");document.body.style.overflow="hidden"; } function renderOfficers(){ const rows=USERS.map(u=>{ const canRemove=u.role!=="admin" && (!SESSION||u.username!==SESSION.username); return ` ${esc(u.name)} ${u.zone?`${u.zone}`:"—"} ${esc(u.username)} ${u.role.toUpperCase()} ${canRemove?``:""} `; }).join(""); const zoneOpts=ZONES.map(z=>``).join(""); document.getElementById("offBody").innerHTML=`
➕ Add New Investigating Officer

Removing an officer revokes their login but does not delete cases they are assigned to.

${rows}
OfficerZoneUsernameRoleActions
`; } function previewIoUsername(){ const n=document.getElementById("newIoName").value.trim(); const z=document.getElementById("newIoZone").value; const el=document.getElementById("newIoUsernamePreview"); if(!n||!z){el.value="";return;} el.value=n.replace(/^PI\s+/i,"").replace(/\s+/g,"").toLowerCase()+"."+(ZONE_CODE[z]||"x").toLowerCase(); } function clearNewIoForm(){ document.getElementById("newIoName").value=""; document.getElementById("newIoZone").value=""; document.getElementById("newIoUsernamePreview").value=""; document.getElementById("newIoName").focus(); } async function addOfficerInline(){ let name=document.getElementById("newIoName").value.trim(); const zone=document.getElementById("newIoZone").value; if(!name){toast("Officer name is required","warn");document.getElementById("newIoName").focus();return;} if(!zone){toast("Please select a zone","warn");return;} try{ const out=await authApi("create_user",{name,zone}); applyUsers(out.users); toast(`Added ${name} (${zone}) — username: ${out.username}`,"ok"); clearNewIoForm();renderOfficers(); }catch(e){toast(e.message||"Could not add officer","warn");} } async function removeOfficer(username){ if(!confirm(`Remove user ${username}? This revokes their login. This cannot be undone.`))return; try{const out=await authApi("remove_user",{username});applyUsers(out.users);toast(`Removed ${username}`,"ok");renderOfficers();} catch(e){toast(e.message||"Could not remove officer","warn");} } function closeOfficers(){document.getElementById("offOverlay").classList.remove("open");document.body.style.overflow="";} async function setUserPw(un){const np=prompt("Set new password for "+un+":");if(np===null)return;if(np.length<4){toast("Password too short","warn");return;}try{const out=await authApi("set_password",{username:un,password:np});applyUsers(out.users);toast("Password set","ok");}catch(e){toast(e.message||"Error","warn");}} async function resetUserPw(un){try{const out=await authApi("reset_password",{username:un});applyUsers(out.users);toast("Reset to default for "+un,"ok");}catch(e){toast(e.message||"Error","warn");}} /* ===================== EXPORT ===================== */ function exportCSV(){ const list=visibleCases(); const head=["FIR No","Crime Type","PI Name","Zone","FIR Date","Last Update","Days Since FIR","Current Stage","Progress %","Suspect Identified","Arrest Made","FSL Status","Charge Sheet Status","Supervisory Status"]; const rows=list.map(c=>{const si=currentStageIdx(c);return [c.firNo,c.crimeType,c.piName,c.zone,c.firDate,c.lastUpdate,daysBetween(c.firDate)??"",`${si+1} - ${STAGES[si].name}`,overallProgress(c),derived(c,"suspect"),derived(c,"arrest"),derived(c,"fsl"),derived(c,"cs"),statusOf(c).label];}); const csv=[head,...rows].map(r=>r.map(v=>`"${String(v??"").replace(/"/g,'""')}"`).join(",")).join("\n"); const blob=new Blob(["\ufeff"+csv],{type:"text/csv;charset=utf-8"});const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="cybercrime_dashboard_"+new Date().toISOString().slice(0,10)+".csv";a.click();toast("Dashboard exported","ok"); } function isChecked(c,label){let r=false;STAGES.forEach((st,si)=>st.sections.forEach((sc,ci)=>sc.items.forEach((it,ii)=>{if(it===label&&c.checks&&c.checks[itemId(si,ci,ii)])r=true;})));return r;} function derived(c,w){if(w==="suspect")return isChecked(c,"Suspect identified")?"Yes":"No";if(w==="arrest")return isChecked(c,"Arrest completed")?"Yes":"No";if(w==="fsl")return isChecked(c,"Examination report received")?"Received":"Pending";if(w==="cs")return isChecked(c,"Charge Sheet filed")?"Filed":"Pending";return "";} let toastT;function toast(msg,kind){const t=document.getElementById("toast");t.textContent=msg;t.className="toast show "+(kind||"");clearTimeout(toastT);toastT=setTimeout(()=>t.className="toast",2200);} /* ===================== INIT ===================== */ (function init(){ if(!store.persistent){document.getElementById("storageDot").className="dot warn";document.getElementById("storageTxt").textContent="Database offline — local preview only";} loadUsers();loadCases();migrateEvidenceToComplaint();migrateChargesheetShift();initFilters(); const u=window.__CCOE_SERVER_SESSION||null; if(u){SESSION=u;showApp();}else{showLogin();} document.addEventListener("keydown",e=>{ if(e.key==="Escape"){closeCase();closeOfficers();const p=document.getElementById("notifPanel");if(p)p.classList.remove("open");} if(e.key==="Enter"&&document.getElementById("loginView").classList.contains("show"))doLogin(); }); document.addEventListener("click",e=>{const p=document.getElementById("notifPanel");if(p&&p.classList.contains("open")&&!e.target.closest("#notifWrap"))p.classList.remove("open");}); setInterval(()=>{if(SESSION&&document.getElementById("appView").classList.contains("show"))renderSummary();},5*60*1000); window.addEventListener("focus",()=>{if(SESSION&&document.getElementById("appView").classList.contains("show"))renderSummary();}); })();