/* Entry aplikasi — diekstrak dari index.html prototipe agar bisa dibuild.
Di produksi data awal datang dari window.TMS (api.js), bukan data.js. */
const { Button, IconButton, Modal, Field, Input, Icon } = window.TaskFlowDesignSystem_dd53c2;
class Boundary extends React.Component{
constructor(p){super(p);this.state={err:null};}
static getDerivedStateFromError(err){return {err};}
componentDidCatch(err){ if(window.console) console.error("Screen error:",err); }
render(){
if(!this.state.err) return this.props.children;
return (
Halaman ini gagal dimuat
Anda tetap login — coba buka halaman ini lagi. Bila terus terjadi, catat langkahnya untuk tim pengembang.
{String(this.state.err&&this.state.err.message||this.state.err)}
);
}
}
// Halaman yang boleh dibuka tiap role. Ini penjaga di sisi tampilan; server WAJIB memeriksa ulang tiap request.
const ALLOWED_PAGES={
admin:["dashboard","tasks","verify","report","profile","calendar","document","users","audit","absen"],
user:["dashboard","tasks","profile","calendar","document"],
marketing:["dashboard","tasks","profile","calendar","document","absen"],
};
function App(){
const vp=useBreakpoint();
const phone=vp==="mobile";
// Sesi disimpan di sessionStorage (hilang saat tab ditutup) dan hanya menerima nilai role yang dikenal.
// Sesi = akun yang login {id,access}. Data aplikasi sendiri tersimpan di DB (localStorage) → tidak hilang saat refresh.
// Sesi hanya sah bila akunnya masih ada, aktif, dan role-nya belum berubah.
// Saat server aktif, keabsahan sesi ditentukan server (jawaban 401 → keluar otomatis);
// daftar user lokal mungkin belum selesai ditarik saat halaman baru dibuka.
const sessionValid=(s)=>!!s&&((window.TMSNet&&TMSNet.online)?true:(s.access==="admin"?s.id===0:DB.users.some(u=>u.id===s.id&&u.active!==false&&(u.access||"user")===s.access)));
const [session,setSession]=React.useState(()=>{try{const s=JSON.parse(sessionStorage.getItem("bbmc.session")||"null");return sessionValid(s)?s:null;}catch(e){return null;}});
const [page,setPage]=React.useState(()=>{try{const p=sessionStorage.getItem("bbmc.page");return ALLOWED_PAGES.admin.includes(p)?p:"dashboard";}catch(e){return "dashboard";}});
React.useEffect(()=>{try{ if(session) sessionStorage.setItem("bbmc.session",JSON.stringify(session)); else sessionStorage.removeItem("bbmc.session"); }catch(e){}},[session]);
React.useEffect(()=>{try{ sessionStorage.setItem("bbmc.page",page); }catch(e){}},[page]);
// Server aktif: tarik data dari server setelah login, lalu ikuti perubahan dari perangkat lain.
const [syncMsg,setSyncMsg]=React.useState("");
React.useEffect(()=>{
if(!window.TMSNet||!TMSNet.online||!session) return;
const refresh=()=>{setTasks([...DB.tasks]);setAttendance([...DB.attendance]);setComments({...DB.comments});setNotifs([...DB.notifs]);setActivity([...DB.activity]);setAvatars({...DB.avatars});setUserRev(n=>n+1);};
const onErr=(e)=>setSyncMsg(e.detail||"Sinkronisasi gagal");
const bye=()=>{try{sessionStorage.removeItem("bbmc.session");sessionStorage.removeItem("bbmc.page");}catch(e){}setSession(null);setPage("dashboard");};
window.addEventListener("tms:sync",refresh);
window.addEventListener("tms:sync-error",onErr);
window.addEventListener("tms:signed-out",bye);
TMSNet.start().then(()=>{
setSyncMsg("");
// Pemindahan sekali jalan: akun yang dulu hanya ada di perangkat ini dikirim ke server.
if(!DB.usersImported&&DB.users&&DB.users.length){
return TMSNet.importUsers(DB.users).then(()=>{DB.usersImported=true;saveDb();return TMSNet.syncNow();}).catch(()=>{});
}
});
return ()=>{window.removeEventListener("tms:sync",refresh);window.removeEventListener("tms:sync-error",onErr);window.removeEventListener("tms:signed-out",bye);};
},[session]);
const [pending,setPending]=React.useState(null); // role menunggu verifikasi puzzle
const [navPending,startNav]=React.useTransition(); // skeleton hanya saat React masih menyiapkan halaman
const [userRev,setUserRev]=React.useState(0); // penanda perubahan daftar user (USERS dimutasi in-place)
const users=React.useMemo(()=>USERS.slice(),[userRev]);
const [timeline,setTimeline]=React.useState(null); // task yang riwayatnya dibuka
// Sekali jalan saat masuk dashboard: satu lapis fade (opacity+transform saja) lalu dilepas dari DOM.
const [justSignedIn,setJustSignedIn]=React.useState(false);
// Hanya saat benar-benar baru sign in (null → role), bukan saat halaman di-refresh.
const prevSession=React.useRef(session);
React.useEffect(()=>{
const was=prevSession.current; prevSession.current=session;
if(!session||was) return;
setJustSignedIn(true);
const id=setTimeout(()=>setJustSignedIn(false),1200);
return ()=>clearTimeout(id);
},[session]);
const [addOpen,setAddOpen]=React.useState(false);
const [importOpen,setImportOpen]=React.useState(false);
const [annotating,setAnnotating]=React.useState(null);
const [completing,setCompleting]=React.useState(null);
const [pwOpen,setPwOpen]=React.useState(false);
const [setupPhoto,setSetupPhoto]=React.useState(null);
const [setupPw,setSetupPw]=React.useState({next:"",repeat:""});
const [setupErr,setSetupErr]=React.useState("");
const pwStrong=(s)=>s.length>=8&&/[A-Za-z]/.test(s)&&/[0-9]/.test(s);
// Tanda tangan digital Admin: satu file PNG, dipakai untuk semua task ber-TTD.
const [signature,setSignature]=React.useState(DB.signature||null);
const [signing,setSigning]=React.useState(null); // task yang sedang ditandatangani
const [signAuth,setSignAuth]=React.useState(null); // {placement} menunggu konfirmasi password
const [signPw,setSignPw]=React.useState("");
const [signErr,setSignErr]=React.useState("");
const [signView,setSignView]=React.useState(null); // lihat penempatan TTD (read-only)
const [tasks,setTasks]=React.useState(TASKS);
const [attendance,setAttendance]=React.useState(ATTENDANCE);
const [nextId,setNextId]=React.useState(DB.nextTaskId||101);
const [comments,setComments]=React.useState(COMMENTS);
const [thread,setThread]=React.useState(null);
const [editing,setEditing]=React.useState(null);
const [removing,setRemoving]=React.useState(null);
const keyOf=(s)=>`${s.kind}-${s.id}`;
const countOf=(s)=>(comments[keyOf(s)]||[]).length;
const [notifs,setNotifs]=React.useState(NOTIFS);
const [activity,setActivity]=React.useState(ACTIVITY);
const [avatars,setAvatars]=React.useState({});
const setPhoto=(id,url)=>{AVATARS[id]=url;DB.avatars=AVATARS;saveDb();setAvatars({...AVATARS});logAct(id,"photo","Mengganti foto profil",null);};
const [viewing,setViewing]=React.useState(null);
const logAct=(user,kind,title,detail)=>setActivity(a=>[{id:Date.now()+Math.random(),user,kind,title,detail,at:TODAY+" baru saja"},...a]);
// Simpan setiap perubahan ke penyimpanan lokal supaya refresh tidak mengembalikan data.
React.useEffect(()=>{DB.tasks=tasks;DB.attendance=attendance;DB.comments=comments;DB.notifs=notifs;DB.activity=activity;DB.nextTaskId=nextId;saveDb();},[tasks,attendance,comments,notifs,activity,nextId]);
const loginMonth=React.useMemo(()=>monthRange(),[]);
const [range,setRange]=React.useState(loginMonth);
const startTask=(t)=>{patch(t.id,{status:"in_progress",started:t.started||TODAY,progress:taskProgress({...t,status:"in_progress",progress:t.progress||20})});logAct(t.user,"start",`Mulai mengerjakan ${t.type}`,"Status berubah ke In Progress");};
const patch=(id,changes)=>setTasks(ts=>ts.map(t=>t.id===id?{...t,...changes}:t));
const toggleCheck=(task,cid)=>setTasks(ts=>ts.map(t=>{
if(t.id!==task.id||!t.checklist) return t;
const checklist=t.checklist.map(c=>c.id===cid?{...c,done:!c.done}:c);
const next={...t,checklist};
return {...next,progress:taskProgress(next)};
}));
const notify=(n)=>setNotifs(ns=>[{...n,unread:true,time:"baru saja"},...ns]);
// Priority + status pills so the user sees urgency straight from the bell panel.
const prioTag=(p)=>p?{label:p,tone:p==="High"?"red":p==="Medium"?"gold":"neutral",icon:p==="High"?"flame":undefined}:null;
const statusTag=(label,tone,icon)=>({label,tone,icon});
const tagsFor=(task,statusLabel,tone,icon)=>[prioTag(task&&task.priority),statusLabel?statusTag(statusLabel,tone,icon):null].filter(Boolean);
// Reminder otomatis H-3 / H-1 / hari-H + eskalasi ke Admin untuk task lewat target.
const remindedRef=React.useRef(false);
React.useEffect(()=>{
if(!session||remindedRef.current) return;
remindedRef.current=true;
const auto=remindersFor(tasks,TODAY);
if(auto.length) setNotifs(ns=>[...auto,...ns]);
},[session]);
// Halaman di luar hak role (mis. sessionStorage diubah manual) dipaksa kembali ke Dashboard.
React.useEffect(()=>{
if(!session) return;
const ok=ALLOWED_PAGES[session.access]||ALLOWED_PAGES.user;
if(!ok.includes(page)) setPage("dashboard");
},[page,session]);
// Akun dihapus / dinonaktifkan / role diubah saat sesi berjalan → langsung keluar.
React.useEffect(()=>{
if(session&&!sessionValid(session)){try{sessionStorage.removeItem("bbmc.session");sessionStorage.removeItem("bbmc.page");}catch(e){}setSession(null);setPage("dashboard");}
},[session,userRev]);
// Auto sign-out setelah 15 menit tanpa aktivitas — mencegah akun terbuka di komputer bersama.
React.useEffect(()=>{
if(!session) return;
let t;
const bye=()=>{try{sessionStorage.removeItem("bbmc.session");sessionStorage.removeItem("bbmc.page");}catch(e){}setSession(null);setPage("dashboard");};
const reset=()=>{clearTimeout(t);t=setTimeout(bye,15*60*1000);};
const evs=["pointerdown","keydown","wheel","touchstart"];
evs.forEach(e=>window.addEventListener(e,reset,{passive:true}));
reset();
return ()=>{clearTimeout(t);evs.forEach(e=>window.removeEventListener(e,reset));};
},[session]);
if(!session) return pending
? setPending(null)}
onPass={()=>{const a=pending;setPending(null);setSession({id:a.id,access:a.access});setPage("dashboard");setPwOpen(!!a.mustChange);}} />
: setPending(acc)} />;
const isAdmin=session.access==="admin";
const allowed=ALLOWED_PAGES[session.access]||ALLOWED_PAGES.user;
const meId=session.id;
const safePage=allowed.includes(page)?page:"dashboard";
// Role user only ever sees notifications addressed to the account that is logged in.
const myNotifs=isAdmin?notifs:notifs.filter(n=>n.user===meId);
const TITLES={dashboard:"Dashboard",tasks:"Task List",add:"Add New Task",verify:"Task Verification",report:"Report",profile:"Profile",calendar:"Calendar",document:"Document",users:"Manajemen User",audit:"Log Activity",absen:isAdmin?"Absen Marketing":"Absen Kunjungan"};
const periodPages=["dashboard","tasks","calendar"];
const periodFilter = periodPages.includes(safePage)
?
: null;
const actions = isAdmin
? <>{periodFilter}
{!phone && }
{safePage==="dashboard" && exportDashboardPdf(tasks,range)} />}
{!phone && setImportOpen(true)} />}
{!phone && setAddOpen(true)} />}>
: <>{periodFilter}
{!phone && }
{phone
? setPage("calendar")} />
: }>;
// user submits work → goes to On Verification, never straight to Done
const submitForVerification=(task,{doc,docs,reason,note})=>{
const revRound=(task.revRound||0)+(task.status==="revision"?1:0);
const files=(docs&&docs.length?docs:doc?[doc]:[]);
const versions=[...(task.versions||[]),...(files.length?[{round:(task.versions?task.versions.length:0)+1,at:TODAY+" baru saja",note:note||null,files}]:[])];
patch(task.id,{status:"on_verification",revRound,versions,progress:taskProgress({...task,status:"on_verification",revRound}),evidence:doc,evidences:docs,submitted:TODAY,delay:reason||task.delay,revision:null,userNote:note||null});
logAct(task.user,"submit",`Mengirim bukti ${task.type}`,doc?`${doc.name} · ${Math.round((doc.compressed||doc.size)/1024)} KB`:null);
notify({kind:"task",user:task.user,title:`${task.type} dikirim untuk verifikasi`,detail:"Status berubah ke On Verification",tags:tagsFor(task,"On Verification","teal")});
};
const accept=(task)=>{
// Task wajib TTD belum boleh Done: buka layar TTD dulu, task tetap di Task Verification
// sampai tanda tangan tersimpan (finalizeAccept dipanggil dari applySignature).
if(task.needSign&&!task.signature){ setSigning(task); return; }
// Hemat penyimpanan: setelah diterima, hanya dokumen final yang disimpan —
// berkas proses awal & revisi dihapus (server: unlink file + hapus baris versi).
const dropped=(task.versions||[]).reduce((n,v)=>n+(v.files?v.files.length:0),0);
patch(task.id,{status:"done",progress:100,done:TODAY,versions:null,delegate:null,delegateStatus:null,delegateNote:null,delegateDoneNote:null});
if(dropped>0) logAct(0,"delete",`Menghapus ${dropped} berkas proses/revisi ${task.type}`,"Hanya dokumen final yang diarsipkan");
notify({kind:"system",user:task.user,title:`${task.type} diterima Direktur`,detail:"Status task sekarang Done",tags:tagsFor(task,"Done","leaf","check")});
logAct(0,"accept",`${task.type} diterima Direktur`,`User: ${userById(task.user).name}`);
const nxt=nextOccurrence(task);
if(nxt){
const clone={...task,id:nextId,status:"todo",progress:0,done:null,submitted:null,evidence:null,evidences:null,versions:null,revision:null,revRound:0,userNote:null,delay:null,delegate:null,delegateStatus:null,delegateNote:null,delegateDoneNote:null,
assigned:nxt.assigned,target:nxt.target,
checklist:task.checklist?task.checklist.map(c=>({...c,done:false})):undefined};
setNextId(n=>n+1);
setTasks(ts=>[clone,...ts]);
notify({kind:"task",user:task.user,title:`Task berulang dibuat: ${task.type}`,detail:`Periode berikutnya · target ${nxt.target}`,tags:[prioTag(task.priority),statusTag(REPEAT_LABEL[task.repeat],"mint","repeat")].filter(Boolean)});
logAct(0,"create",`Task berulang ${task.type} dibuat otomatis`,`${REPEAT_LABEL[task.repeat]} · target ${nxt.target}`);
}
};
const createTasks=({type,category,priority,repeat,needSign,checklist,users,assigned,target,isProject,projectName,rows})=>{
// Multi-user create: `rows` carries one {type,user} pair per task under one project.
const list=rows&&rows.length?rows:users.map((uid)=>({type,user:uid}));
const pname=isProject?(projectName&&projectName.trim()?projectName.trim():type):null;
const created=list.map((r,i)=>({
id:nextId+i, type:r.type||type, user:r.user, category, priority, repeat:repeat&&repeat!=="none"?repeat:undefined, status:"todo",
needSign:!!needSign,
checklist:checklist&&checklist.length?checklist.map(c=>({...c})):undefined,
checks:checklist&&checklist.length?`0/${checklist.length}`:"0/10",
assigned, target, done:null, progress:0, checks:"0/10", comments:0, tint:"white",
project:pname,
}));
setNextId(n=>n+created.length);
setTasks(ts=>[...created,...ts]);
const label=pname||type;
notify({kind:"task",user:list[0].user,title:`Task baru: ${label}`,detail:`${isProject?"Project untuk "+created.length+" task":"Ditugaskan ke "+nameOf(list[0].user)} · target ${target}`,tags:[prioTag(priority),statusTag("To Do","neutral"),isProject?statusTag("Project","teal","folder"):null].filter(Boolean)});
logAct(0,"create",`Membuat ${isProject?"project":"task"} ${label}`,`${isProject?created.map(c=>`${c.type} → ${nameOf(c.user)}`).join(" · "):"Ditugaskan ke "+nameOf(list[0].user)} · target ${target}`);
setAddOpen(false);
setPage("tasks");
};
const openTaskThread=(t)=>setThread({kind:"task",id:t.id,title:t.type,subtitleTask:t});
const openProjectThread=(p)=>setThread({kind:"project",id:p.name,title:p.name,members:p.members,allowed:p.members});
const sendMessage=(text,file)=>{
const k=keyOf(thread);
const author=isAdmin?{by:0,name:"Direktur Klinik",role:"Direktur"}:{by:meId,name:nameOf(meId),role:roleOf(meId)};
setComments(c=>({...c,[k]:[...(c[k]||[]),{...author,at:"baru saja",text,file}]}));
if(file) notify({kind:"task",user:meId,title:`Lampiran baru di ${thread.title}`});
const mentioned=parseMentions(text).filter(id=>id!==meId);
mentioned.forEach(id=>notify({kind:"mention",user:id,title:`Anda disebut di ${thread.title}`,detail:`${author.name}: ${String(text).slice(0,80)}`,tags:[statusTag("Mention","teal","at-sign")]}));
if(!mentioned.length&&isAdmin) notify({kind:"mention",user:0,title:`Komentar baru di ${thread.title}`});
if(mentioned.length) logAct(meId,"comment",`Menyebut ${mentioned.map(nameOf).join(", ")} di ${thread.title}`,String(text).slice(0,80));
logAct(meId,file?"upload":"comment",`${file?"Melampirkan dokumen di":"Menulis komentar di"} ${thread.title}`,file?file.name:text);
};
const updateTask=({type,category,priority,repeat,needSign,checklist,assigned,target})=>{
// keep the ticks the user already made; new rows arrive unchecked
const prev=editing.checklist||[];
const merged=checklist&&checklist.length
? checklist.map(c=>({...c,done:!!(prev.find(p=>p.label===c.label)||{}).done}))
: undefined;
const next={...editing,type,category,priority,repeat:repeat&&repeat!=="none"?repeat:undefined,assigned,target,checklist:merged};
patch(editing.id,{type,category,priority,repeat:next.repeat,needSign:!!needSign,assigned,target,checklist:merged,progress:taskProgress(next)});
setEditing(null);
notify({kind:"system",user:0,title:`Task diperbarui: ${type}`,detail:`Target ${target}`,tags:[prioTag(priority)].filter(Boolean)});
logAct(0,"edit",`Mengubah task ${type}`,`Target ${target}`);
};
const updateProject=(project,{name,category,priority,target})=>{
const ids=new Set(project.tasks.map(t=>t.id));
setTasks(ts=>ts.map(t=>ids.has(t.id)?{...t,project:name,category,priority,target}:t));
notify({kind:"system",user:0,title:`Project diperbarui: ${name}`,detail:`${project.tasks.length} task ikut diperbarui · target ${target}`,tags:[prioTag(priority),statusTag("Project","teal","folder")].filter(Boolean)});
logAct(0,"edit",`Mengubah project ${project.name}`,`${name} · ${category} · ${priority} · target ${target}`);
};
const deleteProject=(project)=>{
const ids=new Set(project.tasks.map(t=>t.id));
setTasks(ts=>ts.filter(t=>!ids.has(t.id)));
notify({kind:"system",user:0,title:`Project dihapus: ${project.name}`,detail:`${project.tasks.length} task ikut dihapus`});
logAct(0,"delete",`Menghapus project ${project.name}`,`${project.tasks.length} task · ${project.members.map(nameOf).join(", ")}`);
};
const deleteTask=(t)=>{
setTasks(ts=>ts.filter(x=>x.id!==t.id));
setRemoving(null);
notify({kind:"system",user:0,title:`Task dihapus: ${t.type}`,detail:"Task dibatalkan oleh Direktur"});
logAct(0,"delete",`Menghapus task ${t.type}`,`User: ${userById(t.user).name}`);
};
const delegateRevision=(task,userId,note)=>{
patch(task.id,{delegate:userId,delegateStatus:"pending",delegateNote:note||null});
notify({kind:"mention",user:userId,title:`Diminta bantu revisi: ${task.type}`,detail:`Dari Direktur untuk ${nameOf(task.user)}`,tags:tagsFor(task,"Delegasi Revisi","coral","users")});
logAct(0,"delegate",`Mendelegasikan revisi ${task.type}`,`Ke ${userById(userId).name}${note?" · "+note:""}`);
};
const revisionDone=(task,note)=>{
patch(task.id,{delegateStatus:"done",delegateDoneNote:note||null,progress:taskProgress({...task,delegateStatus:"done"})});
logAct(meId,"accept",`Menyelesaikan bantuan revisi ${task.type}`,note||`Task milik ${userById(task.user).name}`);
notify({kind:"system",user:task.user,title:`Revision Done: ${task.type}`,detail:"Menunggu Accept dari Direktur",tags:tagsFor(task,"Revision Done","teal")});
};
// Tanda tangan wajib konfirmasi password: klien hanya menahan alurnya,
// server WAJIB memverifikasi ulang password sebelum menyimpan tanda tangan.
const requestSignature=(placement)=>{ setSignPw(""); setSignErr(""); setSignAuth({placement}); };
const applySignature=(placement)=>{
if(!signing) return;
const stamp={...placement,at:TODAY+" baru saja",by:"Direktur Klinik"};
patch(signing.id,{signature:stamp});
if(signing.status!=="done") accept({...signing,signature:stamp});
logAct(0,"accept",`Menandatangani dokumen ${signing.type}`,`Halaman ${stamp.page} · ${signing.evidence?signing.evidence.name:"bukti PDF"}`);
notify({kind:"system",user:signing.user,title:`${signing.type} sudah ditandatangani Direktur`,detail:`Dokumen final tersedia · halaman ${stamp.page}`,tags:[statusTag("Ditandatangani","leaf","signature")]});
setSigning(null);
};
const needRevision=(task,points)=>{
patch(task.id,{status:"revision",revision:points,progress:taskProgress({...task,status:"revision"})});
notify({kind:"late",user:task.user,title:`Revisi diperlukan: ${task.type}`,detail:"Status berubah ke Need Revision",tags:tagsFor(task,"Need Revision","coral","triangle-alert")});
logAct(0,"revision",`Meminta revisi ${task.type}`,points.split("\n")[0]);
};
const saveUser=(mode,id,values)=>{
// Server aktif: akun dibuat/diubah di server (password di-hash di sana) lalu ditarik ulang.
if(window.TMSNet&&TMSNet.online){
TMSNet.saveUser({id:mode==="add"?"":id,name:values.name,email:values.email,role:values.role,position:values.position,access:values.access,pass:values.pass||""})
.then(()=>TMSNet.syncNow())
.then(()=>{setUserRev(n=>n+1);logAct(0,mode==="add"?"upload":"revision",`${mode==="add"?"Membuat":"Memperbarui"} akun ${values.name}`,`${values.email} · ${values.role}`);})
.catch(e=>alert(e.message||"Gagal menyimpan akun."));
return;
}
if(mode==="add"){
const nid=Math.max(DB.nextUserId||1,USERS.reduce((m,u)=>Math.max(m,u.id),0)+1);
DB.nextUserId=nid+1;
USERS.push({id:nid,...values,pass:values.pass||"bbmc2026",color:"var(--teal-500)",active:true,mustChange:true});
logAct(0,"upload",`Membuat akun ${values.name}`,`${values.email} · ${values.role}`);
notify({kind:"system",user:0,title:`Akun baru: ${values.name}`,detail:"Password awal dikirim ke email user"});
}else{
const u=USERS.find(x=>x.id===id);
if(u) Object.assign(u,{...values,pass:values.pass||u.pass});
logAct(0,"revision",`Memperbarui akun ${values.name}`,`${values.email} · ${values.role}`);
}
DB.users=USERS;saveDb();setUserRev(n=>n+1);
};
const toggleUserActive=(u)=>{
if(window.TMSNet&&TMSNet.online){
TMSNet.toggleUser(u.id).then(()=>TMSNet.syncNow()).then(()=>{
setUserRev(n=>n+1);
logAct(0,"edit",`Mengubah status akun ${u.name}`,u.email);
}).catch(e=>alert(e.message||"Gagal mengubah status akun."));
return;
}
const t=USERS.find(x=>x.id===u.id);
if(t) t.active=t.active===false;
logAct(0,t&&t.active===false?"delete":"accept",`${t&&t.active===false?"Menonaktifkan":"Mengaktifkan"} akun ${u.name}`,u.email);
notify({kind:"system",user:u.id,title:`Status akun Anda diubah`,detail:t&&t.active===false?"Akun dinonaktifkan oleh Direktur":"Akun diaktifkan kembali"});
DB.users=USERS;saveDb();setUserRev(n=>n+1);
};
const deleteUser=(u)=>{
if(window.TMSNet&&TMSNet.online){
TMSNet.deleteUser(u.id).then(()=>TMSNet.syncNow()).then(()=>{
setUserRev(n=>n+1);
setTasks(ts=>ts.filter(t=>t.user!==u.id));
setAttendance(a=>a.filter(r=>r.user!==u.id));
logAct(0,"delete",`Menghapus akun ${u.name}`,u.email);
}).catch(e=>alert(e.message||"Gagal menghapus akun."));
return;
}
const i=USERS.findIndex(x=>x.id===u.id);
if(i>=0) USERS.splice(i,1);
logAct(0,"delete",`Menghapus akun ${u.name}`,u.email);
setTasks(ts=>ts.filter(t=>t.user!==u.id));
setAttendance(a=>a.filter(r=>r.user!==u.id));
DB.users=USERS;saveDb();setUserRev(n=>n+1);
};
const resetUserPassword=(u)=>{
if(window.TMSNet&&TMSNet.online){
TMSNet.resetUser(u.id).then(()=>TMSNet.syncNow()).then(()=>{
setUserRev(n=>n+1);
logAct(0,"revision",`Reset password ${u.name}`,"Password kembali ke bbmc2026 · wajib diganti saat login berikutnya");
notify({kind:"system",user:u.id,title:"Password Anda direset",detail:"Login dengan bbmc2026, lalu buat password baru"});
}).catch(e=>alert(e.message||"Gagal reset password."));
return;
}
const t=USERS.find(x=>x.id===u.id);
if(t) t.mustChange=true;
logAct(0,"revision",`Reset password ${u.name}`,"User wajib membuat password baru saat login berikutnya");
notify({kind:"system",user:u.id,title:"Password Anda direset",detail:"Buat password baru saat login berikutnya"});
DB.users=USERS;saveDb();setUserRev(n=>n+1);
};
// Absen kunjungan Marketing: hanya tersimpan bila klien mengirim koordinat GPS.
const saveAbsen=(p)=>{
setAttendance(a=>[{id:Date.now(),user:meId,date:TODAY,...p},...a]);
logAct(meId,"absen",`Absen kunjungan ${p.company}`,`${p.time} · ${p.purpose} · ${fmtCoord(p.lat)}, ${fmtCoord(p.lng)} (±${p.acc} m)`);
notify({kind:"system",user:0,title:`Absen kunjungan: ${nameOf(meId)}`,detail:`${p.company} · ${p.time} · ±${p.acc} m`,tags:[{label:"Absen",tone:"teal",icon:"map-pin-check"}]});
};
const deleteAbsen=(r)=>{
setAttendance(a=>a.filter(x=>x.id!==r.id));
logAct(0,"delete",`Menghapus data absen ${r.company}`,`${nameOf(r.user)} · ${r.date} ${r.time} · ${fmtCoord(r.lat)}, ${fmtCoord(r.lng)}`);
notify({kind:"system",user:r.user,title:"Data absen dihapus Direktur",detail:`${r.company} · ${r.date} ${r.time}`});
};
// Direktur menghapus dokumen final yang salah: berkas + TTD dibuang, task kembali ke revisi.
const deleteDocument=(d,reason)=>{
const t=d.task;
patch(t.id,{status:"revision",progress:taskProgress({...t,status:"revision"}),evidence:null,evidences:null,signature:null,revision:[reason],done:null,submitted:null});
logAct(0,"delete",`Menghapus dokumen final ${d.file.name}`,`${t.type} · ${nameOf(t.user)} · alasan: ${reason}`);
notify({kind:"late",user:t.user,title:`Dokumen dihapus Direktur: ${t.type}`,detail:`${d.file.name} · ${reason}`,tags:tagsFor(t,"Need Revision","coral","triangle-alert")});
};
const purgeLog=({from,to,who})=>{
const before=activity.length;
setActivity(list=>list.filter(e=>!((who==="all"||e.user===who)&&stampInRange(e.at,from,to))));
const removed=activity.filter(e=>(who==="all"||e.user===who)&&stampInRange(e.at,from,to)).length;
logAct(0,"delete",`Menghapus ${removed} baris log activity`,`Rentang ${from} s/d ${to} · ${who==="all"?"semua user":nameOf(who)} · sisa ${before-removed} baris`);
};
let body=null;
if(isAdmin){
if(safePage==="dashboard") body=setAddOpen(true)} onNavigate={setPage} onViewSignature={setSignView} onSignTask={setSigning} />;
else if(safePage==="verify") body=;
else if(safePage==="report") body=;
else if(safePage==="audit") body=;
else if(safePage==="calendar") body=;
else if(safePage==="document") body=;
else if(safePage==="absen") body=;
else if(safePage==="users") body=;
else if(safePage==="profile") body={setSignature(url);DB.signature=url;saveDb();logAct(0,url?"upload":"delete",url?"Mengunggah file tanda tangan digital":"Menghapus file tanda tangan digital",null);}} />;
else body=setAddOpen(true)} onSelectTask={setTimeline}
onComment={openTaskThread} onProjectComment={openProjectThread} commentCount={countOf}
onEdit={(t)=>setEditing(t)} onDelete={(t)=>setRemoving(t)}
onEditProject={updateProject} onDeleteProject={deleteProject} />;
} else if(safePage==="profile"){
body=;
} else if(safePage==="absen"){
body=;
} else if(safePage==="calendar"){
body=;
} else if(safePage==="document"){
body=;
} else {
body=setCompleting(t)}
onResubmit={(t)=>setCompleting(t)}
onComment={openTaskThread} onProjectComment={openProjectThread} commentCount={countOf} onRevisionDone={revisionDone} onTimeline={setTimeline}
onViewEvidence={(t)=>setViewing(t)} />;
}
return (
{if(id==="add"){setAddOpen(true);return;}setThread(null);setCompleting(null);setEditing(null);setRemoving(null);setAddOpen(false);startNav(()=>setPage(id));}} title={TITLES[safePage]||"Dashboard"}
actions={actions}
notifs={myNotifs} onMarkRead={()=>setNotifs(ns=>ns.map(n=>(isAdmin||n.user===1?{...n,unread:false}:n)))}
verifyCount={tasks.filter(t=>t.status==="on_verification").length} taskCount={tasks.length}
onFab={isAdmin?()=>setAddOpen(true):undefined}
onSignOut={()=>{try{if(window.TMSNet&&TMSNet.online)TMSNet.logout();}catch(e){}try{sessionStorage.removeItem("bbmc.session");sessionStorage.removeItem("bbmc.page");}catch(e){}setSession(null);setPage("dashboard");}}>
{justSignedIn && }