(() => { const { Card, Button, IconButton, Badge, Modal, Field, Input, Select, Textarea, DataTable, StatCard, EmptyState, Chip, Icon, Avatar } = window.TaskFlowDesignSystem_dd53c2; /* Absen kunjungan perusahaan — khusus role Marketing. GPS WAJIB aktif: modal langsung meminta izin lokasi perangkat (PC / HP / tablet) dan tombol simpan baru terbuka setelah koordinat benar-benar didapat. Di produksi koordinat dikirim ke server bersama waktu server (bukan waktu perangkat), dan server menolak absen tanpa lat/lng — lihat CODEIGNITER3.md. */ const jamNow = () => { const d = new Date(); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; }; const GPS_HELP = [ { icon: "monitor", label: "PC / Laptop", text: "Klik ikon kunci di address bar → Location → Allow, lalu muat ulang halaman." }, { icon: "smartphone", label: "Android", text: "Aktifkan Lokasi di panel notifikasi, lalu izinkan akses lokasi untuk browser." }, { icon: "tablet", label: "iPhone / iPad", text: "Setelan → Privasi & Keamanan → Layanan Lokasi → aktifkan untuk browser." }, ]; function GpsBadge({ geo }) { const map = { idle: { tone: "neutral", icon: "satellite-dish", text: "GPS belum dicek" }, locating: { tone: "gold", icon: "loader", text: "Mencari lokasi…" }, ok: { tone: "leaf", icon: "map-pin-check", text: `GPS aktif · ±${Math.round(geo.acc || 0)} m` }, denied: { tone: "coral", icon: "map-pin-off", text: "Izin lokasi ditolak" }, unsupported: { tone: "coral", icon: "map-pin-off", text: "Perangkat tanpa GPS" }, insecure: { tone: "coral", icon: "shield-alert", text: "Butuh HTTPS" }, timeout: { tone: "coral", icon: "map-pin-off", text: "Sinyal GPS tidak terbaca" }, error: { tone: "coral", icon: "map-pin-off", text: "Lokasi gagal dibaca" }, }; const s = map[geo.state] || map.idle; const fg = { leaf: "var(--leaf-600)", gold: "var(--gold-600)", coral: "var(--coral-600)", neutral: "var(--neutral-600)" }[s.tone]; return {s.text}; } function AbsenModal({ open, onClose, onSave, companies }) { const [geo, setGeo] = React.useState({ state: "idle" }); const [form, setForm] = React.useState({ company: "", purpose: VISIT_PURPOSES[0], contact: "", note: "" }); const [err, setErr] = React.useState(""); const [clock, setClock] = React.useState(jamNow()); const bp = useBreakpoint(); const narrow = bp === "mobile"; const [blocked, setBlocked] = React.useState(false); const ask = React.useCallback(() => { if (!navigator.geolocation) return setGeo({ state: "unsupported" }); // Geolocation hanya jalan di HTTPS (atau localhost) — di HTTP browser diam saja. if (!window.isSecureContext) return setGeo({ state: "insecure" }); setBlocked(false); setGeo({ state: "locating" }); // PENTING: permintaan lokasi harus dipanggil LANGSUNG (sinkron) di dalam gesture // pengguna. Menunggu `await navigator.permissions.query` lebih dulu membuat // Safari/iOS kehilangan user-activation sehingga popup izin tidak pernah muncul. navigator.geolocation.getCurrentPosition( (p) => setGeo({ state: "ok", lat: p.coords.latitude, lng: p.coords.longitude, acc: p.coords.accuracy }), (e) => { if (e.code === 1) setBlocked(true); setGeo({ state: e.code === 1 ? "denied" : e.code === 3 ? "timeout" : "error", msg: e.message }); }, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 }, ); // Status izin dibaca setelahnya — hanya untuk memilih pesan bantuan. try { if (navigator.permissions) navigator.permissions.query({ name: "geolocation" }) .then((p) => { if (p.state === "denied") setBlocked(true); }).catch(() => {}); } catch (e) {} }, []); const framed = typeof window !== "undefined" && window.self !== window.top; // Popup izin GPS muncul begitu modal dibuka — absen tidak bisa dilanjutkan tanpa lokasi. React.useEffect(() => { if (open) { setForm({ company: "", purpose: VISIT_PURPOSES[0], contact: "", note: "" }); setErr(""); ask(); } }, [open, ask]); React.useEffect(() => { if (!open) return; const t = setInterval(() => setClock(jamNow()), 1000); return () => clearInterval(t); }, [open]); if (!open) return null; const ready = geo.state === "ok"; const failed = ["denied", "unsupported", "timeout", "error", "insecure"].includes(geo.state); const save = () => { if (!ready) return setErr("Lokasi GPS belum didapat. Aktifkan GPS lalu coba lagi."); if (!form.company.trim()) return setErr("Nama perusahaan wajib diisi."); onSave({ company: form.company.trim(), purpose: form.purpose, contact: form.contact.trim(), note: form.note.trim(), lat: geo.lat, lng: geo.lng, acc: Math.round(geo.acc || 0), time: jamNow() }); }; return ( }>
{TODAY} · {clock} Waktu clock in memakai jam server saat disimpan.
{geo.state === "locating" && ( Izinkan akses lokasi pada popup browser untuk melanjutkan. )} {geo.state === "locating" && ( )} {ready && (
{[["Latitude", fmtCoord(geo.lat)], ["Longitude", fmtCoord(geo.lng)], ["Akurasi", `±${Math.round(geo.acc || 0)} meter`], ["Sumber", "GPS perangkat"]].map(([k, v]) => ( {k} {v} ))} Lihat titik absen di peta
)} {failed && (
GPS wajib aktif untuk absen {geo.state === "insecure" && ( Halaman ini dibuka tanpa HTTPS. Browser memblokir GPS di koneksi tidak aman — buka aplikasi lewat alamat https:// lalu coba lagi. )} {framed && ( Halaman sedang dibuka di dalam bingkai (iframe/webview). Buka aplikasi langsung di tab browser agar izin lokasi bisa diminta. )} {blocked && ( Izin lokasi sudah diblokir untuk situs ini, sehingga popup tidak muncul lagi. Buka setelan izin situs (ikon 🔒 / ⓘ di sebelah alamat → Lokasi → Izinkan), lalu tekan Muat Ulang Halaman. )} {GPS_HELP.map((h) => ( {h.label} — {h.text} ))} {blocked && }
)}
{ setForm({ ...form, company: e.target.value }); setErr(""); }} /> setForm({ ...form, contact: e.target.value })} />