CC
Cyber Crime Investigation Dashboard
Gujarat Police · CCOE
Secure sign-in
Username
Sign in
CC
Reset Password
Step 1: Identity Verification
Enter your Username
A One Time Password (OTP) will be sent to the registered email address (abc@xyz.com). It is valid for 10 minutes.
Send OTP
CC
Enter OTP
Step 2: Security Verification
We sent a 6-digit code to abc@xyz.com for .
6-Digit OTP
Verify Code
CC
Create New Password
Step 3: Secure your account
Password reset successfully! Redirecting...
Reset Password
New Case Enter case details and track the investigation
✕
\",\"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 Watch 0 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 Tracker 0 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}
`;
}
/* ===================== 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}
${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",`${z} `));
const fs=document.getElementById("fStage");STAGES.forEach((s,i)=>{if(s.type==="remarks")return;fs.insertAdjacentHTML("beforeend",`${i+1}· ${s.name} `);});
const ft=document.getElementById("fType");CRIME_TYPES.forEach(t=>ft.insertAdjacentHTML("beforeend",`${t} `));
}
/* ===================== 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=`— Select PI — `,matched=false;
if(selPi&&!ioUsers.some(r=>r.pi===selPi&&(!selZone||r.zone===selZone)))
html+=`${esc(selPi)} (existing) `;
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+=`${esc(r.pi)} `;});
html+=` `;});
return html;
}
function zoneOptions(sel){let h=`— Zone — `;ZONES.forEach(z=>h+=`${z} `);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()
? `Investigating Officer (PI) ${piOptions(c.piName,c.zone)}
`
: `Investigating Officer (PI)
`;
const zoneField=isAdmin()
? `Zone ${zoneOptions(c.zone)}
`
: `Zone
`;
let html=`
Case Tracking Fields
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 `${esc(it)} `;}).join("")}`).join("");}
function instanceBlock(si,inst,count){
const id=stageIdOf(si),meta=REPEAT_META[id];
const rm=count>1?`✕ Remove `:"";
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+=`➕ Add another ${meta.word} `;
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?`Edit Delete
`:"";
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=`
Mark as done — clears this case from the 🔔 notifications
✓ Done
`;
}
const footer=canEdit
? `
➕ Add remark
`
: `🔒 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}
▶
${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 `
📅 Arrest date
⏱ Limit
90 days
60 days
⚖️ Bail granted?
No
Yes
${info.txt}
⚖️ Charge sheet filed
`;
}
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()}
Set PW
Reset
${canRemove?`Remove `:""}
`;
}).join("");
const zoneOpts=ZONES.map(z=>`${z} `).join("");
document.getElementById("offBody").innerHTML=`
➕ Add New Investigating Officer
Add Officer
Clear
Removing an officer revokes their login but does not delete cases they are assigned to.
Officer Zone Username Role Actions ${rows}
`;
}
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();});
})();