<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nikahyuk Client Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style> body { font-family: 'Inter', sans-serif; } </style>
</head>
<body class="bg-gray-50">
<header class="bg-white shadow p-4 flex justify-between">
<h1 class="font-bold text-teal-600">Nikahyuk</h1>
<button onclick="logout()" class="text-xs bg-teal-600 text-white px-3 py-1 rounded">Logout</button>
</header>
<div id="loginScreen" class="p-6">
<div class="bg-white p-6 rounded-xl shadow max-w-sm mx-auto">
<h2 class="font-bold mb-4 text-center">Login Client</h2>
<input id="username" placeholder="Username" class="w-full p-3 mb-3 border rounded">
<button onclick="login()" class="w-full bg-teal-600 text-white p-3 rounded">Masuk</button>
</div>
</div>
<main id="app" class="hidden p-4 space-y-4">
<div class="bg-teal-600 text-white p-4 rounded-xl">
<p id="namaManten" class="font-bold"></p>
<p id="tanggalNikah" class="text-sm"></p>
<p id="venueAcara" class="text-xs mt-1 opacity-90"></p>
</div>
<div id="dynamicContent" class="space-y-4"></div>
</main>
<div id="loading" class="fixed inset-0 bg-black/40 flex items-center justify-center hidden">
<div class="bg-white p-5 rounded-xl">
<div class="animate-spin h-8 w-8 border-4 border-teal-500 border-t-transparent rounded-full"></div>
</div>
</div>
<script>
const loginURL = "https://opensheet.elk.sh/1hMZoZr7gnKss0ogCS8kb18eCH_HUlrgCLbqYLFf5M64/Users";
const dataURL = "https://opensheet.elk.sh/1hMZoZr7gnKss0ogCS8kb18eCH_HUlrgCLbqYLFf5M64/Data";
function showLoading(s){ document.getElementById("loading").classList.toggle("hidden", !s); }
async function login(){
showLoading(true);
const username = document.getElementById("username").value.toLowerCase().trim();
try {
let users = await fetch(loginURL).then(r=>r.json());
if(!Array.isArray(users)) users = Object.values(users);
const user = users.find(u => (u.username||"").toLowerCase().trim() === username);
if(!user){
showLoading(false);
alert("User tidak ditemukan");
return;
}
document.getElementById("namaManten").innerText = user.nama_manten;
document.getElementById("tanggalNikah").innerText = user.tanggal;
document.getElementById("venueAcara").innerText = "Venue: " + (user.venue || "-");
localStorage.setItem("client", username);
document.getElementById("loginScreen").classList.add("hidden");
document.getElementById("app").classList.remove("hidden");
await loadData(username);
} catch(e) {
console.log("ERROR LOGIN:", e);
alert("Gagal login");
showLoading(false);
}
}
async function loadData(username){
showLoading(true);
try{
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const dataRes = await fetch(dataURL, { signal: controller.signal });
clearTimeout(timeout);
let data = await dataRes.json();
if(!Array.isArray(data)) data = Object.values(data);
data = data.filter(i => (i.client||"").toLowerCase().trim() === username);
let html = "";
const categories = {};
data.forEach(i => {
let type = (i.type || "").toLowerCase().trim();
if(!type && (i.value||"").trim() !== "") type = "vendor";
if(!type) type = "lainnya";
if(!categories[type]) categories[type] = [];
categories[type].push(i);
});
Object.keys(categories).forEach(cat => {
let itemsHTML = "";
if(cat === "timeline"){
const total = categories[cat].length;
const done = categories[cat].filter(x => {
const s = (x.status||"").toLowerCase().trim();
return s === "done" || s === "selesai" || s === "complete";
}).length;
const percent = total ? Math.round((done/total)*100) : 0;
itemsHTML += `
<div class="bg-white p-3 rounded shadow">
<div class="mb-2">
<div class="flex justify-between text-xs mb-1">
<span>Progress Persiapan</span>
<span>${percent}%</span>
</div>
<div class="w-full bg-gray-200 h-2 rounded">
<div class="bg-teal-500 h-2 rounded" style="width:${percent}%"></div>
</div>
</div>
`;
categories[cat].forEach(i => {
itemsHTML += `
<div class="bg-gray-50 p-3 rounded mt-2 space-y-2">
<input value="${i.value || ''}" class="w-full text-xs border p-1 rounded">
<p class="font-medium">${i.name || '-'}</p>
<p class="text-xs text-gray-400">${i.status || '-'}</p>
<button onclick="updateTimeline(this,'${username}','${i.name||''}')" class="bg-teal-600 text-white text-xs px-2 py-1 rounded">Update Tanggal</button>
</div>`;
});
itemsHTML += `</div>`;
}
else if(cat === "vendor"){
const groupedVendor = {};
categories[cat].forEach(v => {
const vName = (v.nama_vendor || v.value || '-').trim();
if(!groupedVendor[vName]) groupedVendor[vName] = [];
groupedVendor[vName].push(v);
});
Object.keys(groupedVendor).forEach(vendor => {
const items = groupedVendor[vendor];
let listItems = "";
let isBooked = false;
items.forEach(it => {
if((it.status||"").toLowerCase() === "booked") isBooked = true;
listItems += `
<li class="flex justify-between text-sm border-b py-1 last:border-none">
<span>${it.name || '-'}</span>
<span class="text-gray-400">${it.status || '-'}</span>
</li>`;
});
const status = isBooked ? "Sudah Dibooking" : "Belum";
const color = isBooked ? "text-green-500" : "text-red-500";
itemsHTML += `
<div class="bg-gray-50 p-3 rounded">
<div class="flex justify-between mb-2">
<span class="font-medium">${vendor}</span>
<span class="${color}">${status}</span>
</div>
<ul class="pl-4 list-disc text-gray-600">
${listItems}
</ul>
</div>`;
});
}
else{
categories[cat].forEach(i => {
itemsHTML += `
<div class="bg-gray-50 p-3 rounded">
<p class="font-medium">${i.name || i.value || '-'}</p>
<p class="text-xs text-gray-400">${i.status || ''}</p>
<p class="text-xs text-gray-500">${i.value || ''}</p>
</div>`;
});
}
html += `
<div class="bg-white p-4 rounded-xl shadow">
<h3 class="font-bold mb-2 capitalize">${cat}</h3>
<div class="space-y-2">${itemsHTML}</div>
</div>`;
});
document.getElementById("dynamicContent").innerHTML = html;
}catch(e){
console.log("ERROR LOAD DATA:", e);
document.getElementById("dynamicContent").innerHTML = "<p class='text-center text-red-500'>Gagal load data</p>";
}finally{
showLoading(false);
}
}
// FIXED: no extra parenthesis
async function updateTimeline(btn, username, name){
const parent = btn.parentElement;
const input = parent.querySelector('input');
const value = input.value;
showLoading(true);
try{
await fetch("https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec", {
method: "POST",
body: JSON.stringify({
client: username,
name: name,
value: value
})
});
alert("Tanggal berhasil diupdate");
}catch(e){
alert("Gagal update");
}
showLoading(false);
}
function logout(){ localStorage.clear(); location.reload(); }
window.onload = ()=>{
const u = localStorage.getItem("client");
if(u){
document.getElementById("loginScreen").classList.add("hidden");
document.getElementById("app").classList.remove("hidden");
loadData(u);
}
}
</script>
</body>
</html>