/* 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 && }
{body}
{signing && t.id===signing.id)||signing} signature={signature} onClose={()=>setSigning(null)} onSign={requestSignature} />} {signView && setSignView(null)} />} {signAuth && setSignAuth(null)} footer={<> }>
{setSignPw(e.target.value);setSignErr("");}} /> Setiap pembubuhan tanda tangan tercatat di Log Activity beserta waktu dan halamannya.
} setImportOpen(false)} onCreate={createTasks} /> {annotating && setAnnotating(null)} onSubmit={(t,points,pins)=>{patch(t.id,{annotations:pins});needRevision(t,points);setAnnotating(null);}} />} setAddOpen(false)} onCreate={createTasks} tasks={tasks} /> {editing && setEditing(null)} onCreate={updateTask} />} {removing && setRemoving(null)} description={`"${removing.type}" akan dihapus. Riwayat diskusinya juga tidak lagi dapat diakses.`} footer={<> } />} {viewing && setViewing(null)} />} {timeline && t.id===timeline.id)||timeline} messages={comments[keyOf({kind:"task",id:timeline.id})]||[]} onOpenDiscussion={openTaskThread} onViewEvidence={setViewing} onClose={()=>setTimeline(null)} />} {thread && setThread(null)} />} {completing && setCompleting(null)} onSubmit={(payload)=>{submitForVerification(completing,payload||{});setCompleting(null);}} />} { if(!setupPhoto) return setSetupErr("Foto profil wajib diunggah."); if(!pwStrong(setupPw.next)) return setSetupErr("Password baru minimal 8 karakter dan memuat huruf serta angka."); if(setupPw.next!==setupPw.repeat) return setSetupErr("Ulangi password tidak sama."); setPhoto(meId,setupPhoto); const acc=isAdmin?DB.direktur:USERS.find(u=>u.id===meId); if(window.TMSNet&&TMSNet.online){ TMSNet.setPassword("",setupPw.next).then(()=>{ if(acc) acc.mustChange=false; logAct(meId,"password","Mengganti password login pertama",null); setSetupPw({next:"",repeat:""});setSetupErr("");setPwOpen(false); }).catch(e=>setSetupErr(e.message||"Gagal menyimpan password.")); return; } if(acc){acc.pass=setupPw.next;acc.mustChange=false;} DB.users=USERS;saveDb(); logAct(meId,"password","Mengganti password login pertama",null); setSetupPw({next:"",repeat:""});setSetupErr("");setPwOpen(false); }}>Simpan & Lanjutkan}>
{setupPhoto ? Foto profil : }
{setupPhoto?"Foto siap disimpan.":"Wajib · JPG atau PNG · dipakai sebagai avatar di seluruh sistem."}
{setSetupPw({...setupPw,next:e.target.value});setSetupErr("");}} /> {setSetupPw({...setupPw,repeat:e.target.value});setSetupErr("");}} /> {setupErr && {setupErr} }
); } ReactDOM.createRoot(document.getElementById("root")).render();