"use client";

import Image from "next/image";
import Link from "next/link";
import { FormEvent, useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import { BusinessSettings, defaultBusinessSettings } from "../lib/business-settings";
import { CmsAnalyticsDashboard } from "./cms-analytics-dashboard";
import { Brand } from "./public-ui";
import { Icon } from "./icons";

type CmsUser={id:string;name:string;email:string;role:"super_admin"|"article_writer"|"sales_promotion"};
type LeadRecord={id:string;name:string;whatsapp?:string;email?:string|null;location?:string;building_type?:string;service:string;quantity?:number|null;urgency?:string;description?:string;photo_media_id?:string|null;photo_filename?:string|null;photo_mime_type?:string|null;photo_size?:number|null;requested_schedule?:string|null;status:string;assigned_to?:string|null;utm_source?:string|null;utm_medium?:string|null;utm_campaign?:string|null;utm_content?:string|null;utm_term?:string|null;landing_page?:string|null;created_at:string;updated_at?:string};
type ServiceRecord={id:string;title:string;slug:string;excerpt:string;contentBody:string;icon:string;status:string;sortOrder:number;seoTitle:string;metaDescription:string;updatedAt:string};
type MediaRecord={id:string;filename:string;mimeType:string;size:number;width:number|null;height:number|null;altText:string;caption:string;createdAt:string};
type TaxonomyRecord={id:string;type:"category"|"tag";name:string;slug:string;description:string;robotsIndex:boolean;usageCount:number};
type ArticleRecord={id:string;title:string;slug:string;excerpt:string;contentBody:string;featuredImageId:string|null;categoryIds:string[];tagIds:string[];status:string;focusKeyword:string;seoTitle:string;metaDescription:string;canonicalUrl:string;robotsIndex:boolean;robotsFollow:boolean;scheduledAt:string;publishedAt:string;readingMinutes:number;authorName:string;authorId:string|null;updatedAt:string};
type FaqRecord={id:string;question:string;answer:string;category:string;sortOrder:number;active:boolean;updatedAt:string};
type PromotionRecord={id:string;title:string;slug:string;description:string;bannerMediaId:string|null;bannerFilename:string;bannerAltText:string;ctaLabel:string;ctaUrl:string;whatsappTemplate:string;startsAt:string;endsAt:string;status:string;createdBy:string|null;updatedAt:string};
type AreaRecord={id:string;name:string;slug:string;city:string;uniqueIntro:string;localDetails:string;geoLat:number|null;geoLng:number|null;seoTitle:string;metaDescription:string;robotsIndex:boolean;sortOrder:number;active:boolean;updatedAt:string};
type ProjectMediaRecord={id:string;mediaId:string;role:string;sortOrder:number;caption:string;filename:string;altText:string};
type ProjectRecord={id:string;title:string;slug:string;projectType:string;area:string;scope:string;description:string;duration:string;projectYear:number|null;seoTitle:string;metaDescription:string;robotsIndex:boolean;sortOrder:number;status:string;publishedAt:string;updatedAt:string;media:ProjectMediaRecord[]};
type TestimonialRecord={id:string;name:string;photoMediaId:string|null;photoFilename:string;photoAltText:string;serviceType:string;area:string;content:string;source:string;rating:number|null;verified:boolean;active:boolean;sortOrder:number;updatedAt:string};
type Overview={stats:{pages:number,posts:number,leads:number,prices:number},leads:LeadRecord[],prices:Array<{id:string,name:string,price:number|null,unit:string,active:number}>,drafts:Array<{id:string,title:string,status:string,updated_at:string}>};
type BuilderBlock={id:string;type:string;hidden:boolean;headline:string;text:string;background:string;textColor:string;align:"left"|"center"|"right";paddingDesktop:number;paddingTablet:number;paddingMobile:number;radius:number;anchor:string;hideTablet:boolean;hideMobile:boolean};
type PageVersionRecord={id:string;version:number;note:string;createdAt:string;authorName:string};
type PageRecord={id:string;title:string;slug:string;template:string;status:string;authorId:string|null;authorName:string;seoTitle:string;metaDescription:string;canonicalUrl:string;robotsIndex:boolean;robotsFollow:boolean;publishedAt:string;scheduledAt:string;createdAt:string;updatedAt:string;blockCount:number;versionCount:number;blocks?:BuilderBlock[];versions?:PageVersionRecord[]};
type MenuItemRecord={id:string;parentId:string|null;label:string;url:string;position:number;target:"_self"|"_blank";visible:boolean};
type MenuRecord={id:string;name:string;location:"header"|"footer_services"|"footer_information"|"footer_legal";updatedAt:string;persisted:boolean;items:MenuItemRecord[]};
type MenuSuggestion={label:string;url:string};
type RedirectRecord={id:string;fromPath:string;toPath:string;statusCode:301|302;hits:number;active:boolean;createdAt:string;updatedAt:string};
type AuditRecord={id:string;action:string;entityType:string;entityId:string|null;actorId:string|null;actorName:string;actorEmail:string;before:unknown;after:unknown;userAgent:string;createdAt:string};
type AuditResponse={logs:AuditRecord[];summary:{total:number;lastDay:number;loginFailed:number;contentChanges:number};filters:{users:Array<{id:string;name:string;email:string}>;entityTypes:string[]};pagination:{page:number;perPage:number;total:number;totalPages:number};message?:string};
type SecuritySession={id:string;userId:string;userName:string;userEmail:string;role:string;userAgent:string;createdAt:string;expiresAt:string;isCurrent:boolean};
type SecurityProtection={key:string;label:string;detail:string;healthy:boolean};
type SecurityEvent={id:string;action:string;actorName:string;actorEmail:string;userAgent:string;createdAt:string};
type SecurityResponse={summary:{activeSessions:number;failedLogins24h:number;successfulLogins24h:number;lockedSources:number;mfaAdmins:number;mfaAdminsTotal:number};protections:SecurityProtection[];sessions:SecuritySession[];events:SecurityEvent[];message?:string};
type BackupRecord={id:string;filename:string;size:number;encrypted:boolean;formatVersion:number;tableCount:number;recordCount:number;status:string;note:string;createdBy:string;createdByEmail:string;expiresAt:string;completedAt:string;lastVerifiedAt:string;lastRestoredAt:string;restoreCount:number;createdAt:string};
type BackupResponse={backups:BackupRecord[];summary:{available:number;totalSize:number;lastBackupAt:string;totalRestores:number;retentionDays:number};coverage:{database:boolean;mediaFiles:boolean;protectedAuth:boolean};message?:string};

export function CmsLogin(){const [status,setStatus]=useState("");async function submit(e:FormEvent<HTMLFormElement>){e.preventDefault();setStatus("Memverifikasi…");const f=new FormData(e.currentTarget);const res=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(Object.fromEntries(f))});const data=await res.json() as {message:string,redirect?:string};if(res.ok)location.href=data.redirect??"/cms/dashboard";else setStatus(data.message)}return <div className="cms-login"><div className="cms-login-art"><Image src="/images/hero-electrician.png" fill alt="Teknisi listrik memeriksa panel"/><div className="cms-login-copy"><h1>Konten yang rapi. Akses yang terjaga.</h1><p>Kelola halaman, harga, artikel, promo, dan leads Padjajaran Elektrik dari satu tempat.</p></div></div><div className="cms-auth-panel"><form className="cms-auth-box" onSubmit={submit}><Brand/><h2>Masuk ke CMS</h2><p>Super Admin wajib menggunakan kode MFA. Percobaan login dibatasi dan dicatat.</p><label>Email<input className="cms-input" required type="email" name="email" autoComplete="username"/></label><label>Password<input className="cms-input" required type="password" name="password" autoComplete="current-password"/></label><label>Kode MFA (Super Admin)<input className="cms-input" name="totp" inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code"/></label><button className="button" type="submit">Masuk dengan aman</button>{status&&<p className="form-message" role="status">{status}</p>}<p style={{textAlign:"center",marginTop:20}}><Link className="text-link" href="/cms/setup">Setup pertama</Link> · <Link href="/cms/reset-password">Lupa password</Link> · <Link href="/">Website</Link></p></form></div></div>}

export function PasswordReset({ownerEmail}:{ownerEmail:string}){const [status,setStatus]=useState(""),[success,setSuccess]=useState(false);async function resetPassword(e:FormEvent<HTMLFormElement>){e.preventDefault();setStatus("Memverifikasi token pemulihan dan MFA…");const form=e.currentTarget,f=new FormData(form);const res=await fetch("/api/auth/owner-reset",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(Object.fromEntries(f))});const json=await res.json() as {message:string,redirect?:string};setStatus(json.message);if(res.ok){setSuccess(true);form.reset()}}return <div className="cms-login"><div className="cms-login-art"><Image src="/images/hero-electrician.png" fill alt="Teknisi listrik"/><div className="cms-login-copy"><h1>Pemulihan akun pemilik yang aman.</h1><p>Token pemulihan server dan MFA diperlukan sebelum password dapat diganti.</p></div></div><div className="cms-auth-panel"><form className="cms-auth-box" onSubmit={resetPassword}><Brand/><h2>Buat password baru</h2><p>Gunakan token pemulihan dari environment dan kode terbaru dari aplikasi Authenticator.</p><label>Email pemilik<input className="cms-input" readOnly type="email" value={ownerEmail}/></label><label>Token pemulihan<input className="cms-input" required type="password" name="recoveryToken" autoComplete="off"/></label><label>Password baru<input className="cms-input" required type="password" name="password" minLength={12} maxLength={128} autoComplete="new-password"/></label><label>Ulangi password baru<input className="cms-input" required type="password" name="passwordConfirmation" minLength={12} maxLength={128} autoComplete="new-password"/></label><label>Kode MFA<input className="cms-input" required name="totp" inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code"/></label><button className="button" type="submit" disabled={!ownerEmail||success}>{success?"Password berhasil diubah":"Simpan password baru"}</button>{status&&<p className="form-message" role="status">{status}</p>}<p style={{textAlign:"center",marginTop:20}}><Link href="/cms/login">{success?"Masuk dengan password baru":"Kembali ke login"}</Link></p></form></div></div>}

type TotpSetup={secret:string;issuer:string;account:string;otpauth:string};
export function CmsSetup({initialTotp}:{initialTotp:TotpSetup}){
  const [totp,setTotp]=useState<TotpSetup|null>(initialTotp),[status,setStatus]=useState("");
  const setupTokenRef=useRef<HTMLInputElement>(null);
  async function loadTotp(){try{const setupToken=setupTokenRef.current?.value??"";if(!setupToken){setStatus("Masukkan token setup sebelum membuat secret baru.");return}const res=await fetch("/api/auth/totp-secret",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({setupToken}),cache:"no-store"});const data=await res.json() as TotpSetup&{message?:string};if(!res.ok)throw new Error(data.message||"Secret MFA gagal dibuat.");setTotp(data);setStatus("✓ Secret MFA baru berhasil dibuat.")}catch(error){setTotp(null);setStatus(error instanceof Error?error.message:"Secret MFA gagal dibuat.")}}
  async function copySecret(){if(!totp)return;await navigator.clipboard.writeText(totp.secret);setStatus("Secret berhasil disalin. Tambahkan sebagai kunci berbasis waktu di aplikasi Authenticator.")}
  async function submit(e:FormEvent<HTMLFormElement>){e.preventDefault();if(!totp){setStatus("Secret MFA belum tersedia. Muat ulang secret terlebih dahulu.");return}setStatus("Memverifikasi MFA dan membuat Super Admin…");const f=new FormData(e.currentTarget);const res=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({...Object.fromEntries(f),totpSecret:totp.secret})});const data=await res.json() as {message:string};if(res.ok)location.href="/cms/dashboard";else setStatus(data.message)}
  return <div className="cms-login"><div className="cms-login-art"><Image src="/images/electrical-work.png" fill alt="Tim teknisi listrik"/><div className="cms-login-copy"><h1>Setup pertama yang aman.</h1><p>Token setup dari environment diperlukan untuk membuat Super Admin pertama.</p></div></div><div className="cms-auth-panel"><form className="cms-auth-box" onSubmit={submit}><Brand/><h2>Buat Super Admin</h2><p>Tambahkan secret yang dibuat server ke aplikasi authenticator. Pilih tipe <strong>berbasis waktu</strong>, SHA-1, 6 digit, dan periode 30 detik.</p><label>Token setup<input ref={setupTokenRef} className="cms-input" required type="password" name="setupToken" autoComplete="off"/><small>Nilainya sama dengan <code>SETUP_TOKEN</code> di file environment server.</small></label><label>Nama<input className="cms-input" required name="name" maxLength={100}/></label><label>Password baru<input className="cms-input" required type="password" name="password" minLength={12} autoComplete="new-password"/></label><label>Secret TOTP<input className="cms-input" readOnly value={totp?.secret??"Sedang dibuat…"}/></label>{totp&&<div style={{display:"flex",gap:8,flexWrap:"wrap",marginTop:-8,marginBottom:12}}><button className="cms-button secondary" type="button" onClick={copySecret}>Salin secret</button><button className="cms-button secondary" type="button" onClick={()=>{setStatus("Membuat secret MFA baru…");void loadTotp()}}>Buat secret baru</button><a className="cms-button secondary" href={totp.otpauth}>Buka Authenticator</a></div>}<p style={{fontSize:11,margin:"0 0 12px"}}>Issuer: {totp?.issuer??"Padjajaran Elektrik"} · Akun: {totp?.account??"email pemilik"}</p><label>Kode 6 digit terbaru<input className="cms-input" required name="totpCode" inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code"/></label><button className="button" disabled={!totp} type="submit">Aktifkan Super Admin</button>{status&&<p className="form-message" role="status">{status}</p>}<p style={{fontSize:11,marginTop:16}}>Gunakan kode yang masih memiliki waktu cukup. Jika hampir berganti, tunggu kode berikutnya. Setup akan terkunci otomatis setelah akun pertama berhasil dibuat.</p></form></div></div>
}

const allNav=[["dashboard","Dashboard","home"],["business","Identitas & Kontak","image"],["pages","Pages","building"],["builder","Page Builder","plus"],["articles","Artikel","cable"],["services","Layanan","bolt"],["prices","Harga","panel"],["promos","Promo","calendar"],["portfolio","Portofolio","image"],["testimonials","Testimoni","check"],["faq","FAQ","plus"],["areas","Area Layanan","ground"],["media","Media Library","upload"],["leads","Leads","phone"],["menus","Menu & Navigasi","menu"],["redirects","Redirect URL","arrow"],["seo","SEO & Integrasi","search"],["users","User & Role","shield"],["audit","Audit Log","check"],["security","Keamanan","shield"],["backup","Backup","upload"]];
const articleNav=[["articles-create","Buat Artikel"],["articles-list","Semua Artikel"],["article-categories","Kategori"],["article-tags","Tags"]];
const blocks=["Hero","Rich text","Gambar + teks","Galeri","Video","Statistik","Benefit cards","Service cards","Pricing table","Proses kerja","Testimoni","Portofolio","FAQ accordion","CTA","Artikel terbaru","Formulir","Google Maps","Logo pelanggan","Area layanan","Spacer / divider"];

export function CmsDashboard({user}:{user:CmsUser}){
  const [active,setActive]=useState("dashboard"),[articleMenuOpen,setArticleMenuOpen]=useState(false),[data,setData]=useState<Overview|null>(null),[message,setMessage]=useState(""),[editingPageId,setEditingPageId]=useState<string|null>(null),[builderInstance,setBuilderInstance]=useState(0);
  useEffect(()=>{fetch("/api/cms/overview").then(r=>r.json()).then(setData).catch(()=>setMessage("Data belum dapat dimuat."))},[]);
  const visibleNav=allNav.filter(([key])=>user.role==="super_admin"||user.role==="article_writer"&&["dashboard","articles","media","seo"].includes(key)||user.role==="sales_promotion"&&["dashboard","pages","builder","promos","leads","media"].includes(key));
  const activeTitle=articleNav.find(([key])=>key===active)?.[1]??visibleNav.find(([key])=>key===active)?.[1]??"Dashboard";
  function openBuilder(id:string|null){setEditingPageId(id);setBuilderInstance(current=>current+1);setActive("builder")}
  async function logout(){await fetch("/api/auth/logout",{method:"POST"});location.href="/cms/login"}
  return <div className="cms-body"><div className="cms-shell"><aside className="cms-sidebar"><Brand/><nav className="cms-nav">{visibleNav.map(([key,label,icon])=>key==="articles"?<div className={`cms-nav-group ${active.startsWith("article")?"active":""}`} key={key}><button type="button" aria-expanded={articleMenuOpen} onClick={()=>setArticleMenuOpen(current=>!current)}><Icon name={icon}/><span>{label}</span><b aria-hidden="true">{articleMenuOpen?"−":"+"}</b></button>{articleMenuOpen&&<div className="cms-subnav">{articleNav.map(([childKey,childLabel])=><a href={`#${childKey}`} className={active===childKey?"active":""} key={childKey} onClick={event=>{event.preventDefault();setActive(childKey)}}>{childLabel}</a>)}</div>}</div>:<a href={`#${key}`} className={active===key?"active":""} key={key} onClick={(event)=>{event.preventDefault();if(key==="builder"){setEditingPageId(null);setBuilderInstance(current=>current+1)}setActive(key)}}><Icon name={icon}/><span>{label}</span></a>)}</nav></aside><main className="cms-main"><header className="cms-topbar"><div><span className="eyebrow">Content Management System</span><h1>{activeTitle}</h1></div><div className="cms-user"><i/><span><strong>{user.name}</strong><br/>{user.role.replaceAll("_"," ")}</span><button className="cms-button secondary" onClick={logout}>Keluar</button></div></header>{message&&<p className="form-message error">{message}</p>}{active==="dashboard"&&<CmsAnalyticsDashboard data={data} userRole={user.role} onOpenIntegrations={()=>setActive("seo")}/>} {active==="business"&&user.role==="super_admin"&&<BusinessSettingsManager/>} {active==="pages"&&<PageManager user={user} onEdit={openBuilder}/>} {active==="builder"&&<PageBuilder key={builderInstance} user={user} pageId={editingPageId} onBack={()=>setActive("pages")} onSaved={setEditingPageId}/>} {active==="services"&&user.role==="super_admin"&&<ServiceManager/>} {active==="prices"&&<PriceManager prices={data?.prices??[]}/>} {active==="articles-create"&&<ArticleManager user={user} mode="create" onNavigate={setActive}/>} {active==="articles-list"&&<ArticleManager user={user} mode="list" onNavigate={setActive}/>} {active==="article-categories"&&<TaxonomyManager user={user} view="category"/>} {active==="article-tags"&&<TaxonomyManager user={user} view="tag"/>} {active==="promos"&&<PromoManager onOpenMedia={()=>setActive("media")}/>} {active==="portfolio"&&user.role==="super_admin"&&<PortfolioManager onOpenMedia={()=>setActive("media")}/>} {active==="testimonials"&&user.role==="super_admin"&&<TestimonialManager onOpenMedia={()=>setActive("media")}/>} {active==="faq"&&user.role==="super_admin"&&<FaqManager/>} {active==="areas"&&user.role==="super_admin"&&<AreaManager/>} {active==="media"&&<MediaManager user={user}/>} {active==="leads"&&<LeadManager leads={data?.leads??[]} user={user}/>} {active==="menus"&&user.role==="super_admin"&&<MenuManager/>} {active==="redirects"&&user.role==="super_admin"&&<RedirectManager/>} {active==="seo"&&user.role==="super_admin"&&<IntegrationManager/>} {active==="users"&&user.role==="super_admin"&&<UserManager/>} {active==="audit"&&user.role==="super_admin"&&<AuditLogManager/>} {active==="security"&&user.role==="super_admin"&&<SecurityManager/>} {active==="backup"&&user.role==="super_admin"&&<BackupManager/>} {!["dashboard","business","pages","builder","services","prices","articles-create","articles-list","article-categories","article-tags","promos","portfolio","testimonials","faq","areas","media","leads","menus","redirects","seo","users","audit","security","backup"].includes(active)&&<ModulePlaceholder name={activeTitle}/>}</main></div></div>
}

const pageStatusLabel:Record<string,string>={draft:"Draft",scheduled:"Terjadwal",published:"Diterbitkan",archived:"Diarsipkan"};
function newBuilderBlock(type:string):BuilderBlock{
  const dark=type==="Hero"||type==="CTA";
  const headline=type==="Hero"?"Jasa Instalasi Listrik di Bogor":type==="CTA"?"Konsultasikan kebutuhan Anda":type==="Service cards"?"Layanan yang tersedia":type;
  const text=type==="Hero"?"Konsultasikan kebutuhan instalasi listrik untuk rumah atau tempat usaha.":type==="CTA"?"Kirim lokasi dan foto kondisi untuk mendapatkan arahan awal.":"";
  return{id:crypto.randomUUID(),type,hidden:false,headline,text,background:dark?"#123D32":"#FFFFFF",textColor:dark?"#FFFFFF":"#123D32",align:type==="CTA"?"center":"left",paddingDesktop:56,paddingTablet:40,paddingMobile:28,radius:0,anchor:"",hideTablet:false,hideMobile:false};
}
function normalizeBuilderBlock(block:Partial<BuilderBlock>&Pick<BuilderBlock,"id"|"type">):BuilderBlock{
  const defaults=newBuilderBlock(block.type);
  return{...defaults,...block,id:block.id||defaults.id,type:block.type,align:["left","center","right"].includes(block.align??"")?block.align!:"left"};
}
function defaultPageBlocks():BuilderBlock[]{return["Hero","Service cards","CTA"].map(newBuilderBlock)}
function emptyPage(user:CmsUser):PageRecord{return{id:"",title:"",slug:"",template:user.role==="sales_promotion"?"promotion":"default",status:"draft",authorId:user.id,authorName:user.name,seoTitle:"",metaDescription:"",canonicalUrl:"",robotsIndex:true,robotsFollow:true,publishedAt:"",scheduledAt:"",createdAt:"",updatedAt:"",blockCount:3,versionCount:0,blocks:defaultPageBlocks(),versions:[]}}
function pageSeoScore(page:PageRecord){const title=page.seoTitle.trim()||page.title.trim(),checks=[page.title.trim().length>=5,page.slug.length>=3&&page.slug.length<=75,title.length>=30&&title.length<=60,page.metaDescription.length>=120&&page.metaDescription.length<=160,(page.blocks??[]).filter(block=>!block.hidden).length>=2],passed=checks.filter(Boolean).length;return{passed,total:checks.length,label:passed===5?"SEO baik":passed>=3?"Perlu dilengkapi":"SEO masih lemah",className:passed===5?"seo-good":passed>=3?"seo-warning":"seo-poor"}}
function pageDate(value:string){if(!value)return"—";const date=new Date(value);return Number.isNaN(date.getTime())?"—":date.toLocaleString("id-ID",{dateStyle:"medium",timeStyle:"short"})}
function pageDatetimeInput(value:string){if(!value)return"";const date=new Date(value);if(Number.isNaN(date.getTime()))return"";return new Date(date.getTime()-date.getTimezoneOffset()*60000).toISOString().slice(0,16)}

function PageManager({user,onEdit}:{user:CmsUser;onEdit:(id:string|null)=>void}){
  const [items,setItems]=useState<PageRecord[]>([]),[loading,setLoading]=useState(true),[query,setQuery]=useState(""),[filter,setFilter]=useState("all"),[deletingId,setDeletingId]=useState<string|null>(null),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{fetch("/api/cms/pages",{cache:"no-store"}).then(async response=>{const json=await response.json() as {pages?:PageRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Halaman gagal dimuat.");setItems(json.pages??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Halaman gagal dimuat."})).finally(()=>setLoading(false))},[]);
  async function archive(page:PageRecord){setDeletingId(page.id);setNotice({type:"info",message:"Mengarsipkan halaman…"});try{const response=await fetch("/api/cms/pages",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:page.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Halaman gagal diarsipkan.");setItems(current=>current.filter(item=>item.id!==page.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Halaman gagal diarsipkan."})}finally{setDeletingId(null)}}
  const normalized=query.trim().toLowerCase(),visible=items.filter(item=>(filter==="all"||item.status===filter)&&(!normalized||`${item.title} ${item.slug}`.toLowerCase().includes(normalized)));
  return <section className="cms-panel page-manager"><div className="cms-panel-header"><div><h2>Daftar halaman</h2><p>{user.role==="sales_promotion"?"Anda hanya dapat melihat dan mengedit landing page promo milik sendiri.":"Kelola halaman, status publikasi, SEO, block, preview, dan riwayat versi."}</p></div><button className="cms-button" type="button" onClick={()=>onEdit(null)}>＋ Buat halaman</button></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}<div className="page-manager-filters"><label>Cari halaman<input className="cms-input" type="search" value={query} onChange={event=>setQuery(event.target.value)} placeholder="Judul atau slug"/></label><label>Status<select className="cms-input" value={filter} onChange={event=>setFilter(event.target.value)}><option value="all">Semua status</option><option value="draft">Draft</option><option value="scheduled">Terjadwal</option><option value="published">Diterbitkan</option><option value="archived">Diarsipkan</option></select></label><span>{visible.length} halaman</span></div>{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat halaman…</div>:visible.length?<div className="page-admin-list">{visible.map(page=><article className="page-admin-item" key={page.id}><div className="page-admin-icon"><Icon name="building"/></div><div><strong>{page.title}</strong><span>/{page.slug}</span><small>{page.blockCount} block · {page.versionCount} versi · diperbarui {pageDate(page.updatedAt)}</small></div><i className={`lead-status status-${page.status}`}>{pageStatusLabel[page.status]||page.status}</i><div className="page-admin-actions"><button className="cms-button secondary" type="button" onClick={()=>onEdit(page.id)}>Edit</button><a className="cms-button secondary" href={`/${page.slug}?preview=1`} target="_blank" rel="noreferrer">Preview</a>{confirmDeleteId===page.id?<><button className="cms-button secondary" type="button" onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={deletingId!==null} onClick={()=>void archive(page)}>{deletingId===page.id?"Mengarsipkan…":"Ya, arsipkan"}</button></>:<button className="cms-button danger" type="button" disabled={deletingId!==null} onClick={()=>setConfirmDeleteId(page.id)}>Arsipkan</button>}</div></article>)}</div>:<div className="empty-state"><h2>{query||filter!=="all"?"Halaman tidak ditemukan":"Belum ada halaman buatan CMS"}</h2><p>{query||filter!=="all"?"Coba ubah pencarian atau filter status.":"Buat halaman pertama menggunakan block yang telah divalidasi."}</p>{!query&&filter==="all"&&<button className="cms-button" type="button" onClick={()=>onEdit(null)}>Buat halaman pertama</button>}</div>}</section>
}

type BuilderDevice="desktop"|"tablet"|"mobile";
type BuilderInspectorTab="content"|"style"|"advanced";

function BlockVisual({block,device}:{block:BuilderBlock;device:BuilderDevice}){
  const padding=device==="desktop"?block.paddingDesktop:device==="tablet"?block.paddingTablet:block.paddingMobile;
  const style:CSSProperties={padding:`${padding}px ${Math.max(18,Math.round(padding*.6))}px`,backgroundColor:block.background,color:block.textColor,textAlign:block.align,borderRadius:block.radius,opacity:block.hidden?.55:1};
  const title=block.headline||block.type,text=block.text||"Teks pendukung dapat diedit melalui panel Content.";
  if(block.type==="Hero")return <div className="visual-block visual-hero" style={{...style,backgroundImage:`linear-gradient(90deg,${block.background}F2,${block.background}B5),url(/images/hero-electrician.png)`}}><span>Padjajaran Elektrik</span><h2>{title}</h2><p>{text}</p><button type="button">Jadwalkan Survei</button></div>;
  if(block.type==="Gambar + teks")return <div className="visual-block visual-split" style={style}><Image unoptimized src="/images/electrical-work.png" width={440} height={300} alt="Preview pekerjaan listrik"/><div><h2>{title}</h2><p>{text}</p></div></div>;
  if(block.type==="Service cards"||block.type==="Benefit cards")return <div className="visual-block" style={style}><h2>{title}</h2><p>{text}</p><div className="visual-card-grid">{["Instalasi rumah","Panel listrik","Perbaikan korsleting"].map(item=><span key={item}><b>⚡</b>{item}</span>)}</div></div>;
  if(block.type==="Pricing table")return <div className="visual-block" style={style}><h2>{title}</h2><p>{text}</p><div className="visual-price-list"><span>Tambah titik listrik <b>Rp —</b></span><span>Pasang saklar <b>Rp —</b></span><span>Panel listrik <b>Estimasi</b></span></div></div>;
  if(block.type==="FAQ accordion")return <div className="visual-block" style={style}><h2>{title}</h2><p>{text}</p>{["Berapa biaya instalasi?","Apakah tersedia survei?","Area mana yang dilayani?"].map(item=><span className="visual-faq" key={item}>{item}<b>＋</b></span>)}</div>;
  if(block.type==="CTA")return <div className="visual-block visual-cta" style={style}><h2>{title}</h2><p>{text}</p><button type="button">Konsultasikan Kebutuhan</button></div>;
  if(block.type==="Spacer / divider")return <div className="visual-divider" style={{...style,padding:`${Math.max(12,Math.round(padding/2))}px`}}><i/></div>;
  return <div className="visual-block" style={style}><span className="visual-block-type">{block.type}</span><h2>{title}</h2><p>{text}</p><div className="visual-placeholder"><Icon name={block.type==="Formulir"?"phone":block.type==="Portofolio"||block.type==="Galeri"?"image":"building"}/><span>Preview konten terhubung</span></div></div>;
}

function PageBuilder({user,pageId,onBack,onSaved}:{user:CmsUser;pageId:string|null;onBack:()=>void;onSaved:(id:string)=>void}){
  const [page,setPage]=useState<PageRecord>(()=>emptyPage(user));
  const [loading,setLoading]=useState(Boolean(pageId)),[saving,setSaving]=useState(false),[restoringId,setRestoringId]=useState<string|null>(null);
  const [selectedId,setSelectedId]=useState<string|null>(null),[device,setDevice]=useState<BuilderDevice>("desktop"),[inspectorTab,setInspectorTab]=useState<BuilderInspectorTab>("content"),[sidebarTab,setSidebarTab]=useState<"widgets"|"navigator">("widgets"),[widgetSearch,setWidgetSearch]=useState(""),[dragged,setDragged]=useState<number|null>(null);
  const [past,setPast]=useState<BuilderBlock[][]>([]),[future,setFuture]=useState<BuilderBlock[][]>([]),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{if(!pageId)return;fetch(`/api/cms/pages?id=${encodeURIComponent(pageId)}`,{cache:"no-store"}).then(async response=>{const json=await response.json() as {page?:PageRecord;message?:string};if(!response.ok||!json.page)throw new Error(json.message||"Halaman gagal dimuat.");const normalized={...json.page,blocks:(json.page.blocks??[]).map(normalizeBuilderBlock)};setPage(normalized);setSelectedId(null);setPast([]);setFuture([])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Halaman gagal dimuat."})).finally(()=>setLoading(false))},[pageId]);

  const pageBlocks=(page.blocks??[]).map(normalizeBuilderBlock),selected=pageBlocks.find(block=>block.id===selectedId)??null,seo=pageSeoScore({...page,blocks:pageBlocks});
  const allowedWidgets=blocks.filter(type=>(user.role==="super_admin"||!["Video","Statistik","Google Maps","Logo pelanggan"].includes(type))&&type.toLowerCase().includes(widgetSearch.trim().toLowerCase()));
  function change(patch:Partial<PageRecord>){setPage(current=>({...current,...patch}))}
  function applyBlocks(next:BuilderBlock[],remember=true){const normalized=next.map(normalizeBuilderBlock);if(remember){setPast(current=>[...current.slice(-29),pageBlocks]);setFuture([])}change({blocks:normalized,blockCount:normalized.length})}
  function updateSelected(patch:Partial<BuilderBlock>){if(!selectedId)return;applyBlocks(pageBlocks.map(block=>block.id===selectedId?normalizeBuilderBlock({...block,...patch}):block))}
  function add(type:string){const block=newBuilderBlock(type);applyBlocks([...pageBlocks,block]);setSelectedId(block.id);setInspectorTab("content");setSidebarTab("navigator")}
  function move(index:number,direction:-1|1){const target=index+direction;if(target<0||target>=pageBlocks.length)return;const copy=[...pageBlocks],[item]=copy.splice(index,1);copy.splice(target,0,item);applyBlocks(copy)}
  function drop(index:number){if(dragged===null||dragged===index)return;const copy=[...pageBlocks],[item]=copy.splice(dragged,1);copy.splice(index,0,item);applyBlocks(copy);setDragged(null)}
  function undo(){const previous=past.at(-1);if(!previous)return;setFuture(current=>[pageBlocks,...current].slice(0,30));setPast(current=>current.slice(0,-1));change({blocks:previous,blockCount:previous.length});setSelectedId(null)}
  function redo(){const next=future[0];if(!next)return;setPast(current=>[...current,pageBlocks].slice(-30));setFuture(current=>current.slice(1));change({blocks:next,blockCount:next.length});setSelectedId(null)}
  function duplicate(block:BuilderBlock,index:number){const copy={...block,id:crypto.randomUUID(),headline:block.headline?`${block.headline} (salinan)`:block.headline};applyBlocks([...pageBlocks.slice(0,index+1),copy,...pageBlocks.slice(index+1)]);setSelectedId(copy.id)}
  function remove(block:BuilderBlock){const next=pageBlocks.filter(item=>item.id!==block.id);applyBlocks(next);setSelectedId(null)}
  async function save(targetStatus?:string){setSaving(true);const status=targetStatus??page.status;setNotice({type:"info",message:status==="published"?"Menerbitkan halaman…":"Menyimpan perubahan…"});try{const response=await fetch("/api/cms/pages",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({...page,status,template:user.role==="sales_promotion"?"promotion":page.template,blocks:pageBlocks})}),json=await response.json() as {message?:string;page?:PageRecord};if(!response.ok||!json.page)throw new Error(json.message||"Halaman gagal disimpan.");setPage({...json.page,blocks:(json.page.blocks??[]).map(normalizeBuilderBlock)});onSaved(json.page.id);setPast([]);setFuture([]);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Halaman gagal disimpan."})}finally{setSaving(false)}}
  async function restore(version:PageVersionRecord){setRestoringId(version.id);setNotice({type:"info",message:`Memulihkan versi ${version.version}…`});try{const response=await fetch("/api/cms/pages",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({id:page.id,versionId:version.id})}),json=await response.json() as {message?:string;page?:PageRecord};if(!response.ok||!json.page)throw new Error(json.message||"Versi gagal dipulihkan.");setPage({...json.page,blocks:(json.page.blocks??[]).map(normalizeBuilderBlock)});setSelectedId(null);setPast([]);setFuture([]);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Versi gagal dipulihkan."})}finally{setRestoringId(null)}}
  if(loading)return <div className="cms-panel lead-loading"><span className="button-spinner"/> Memuat editor visual…</div>;

  return <div className="builder-workspace elementor-builder">
    <header className="elementor-topbar">
      <div className="elementor-topbar-left"><button className="elementor-icon-button" type="button" onClick={onBack} title="Daftar halaman">←</button><button className={`elementor-icon-button ${selectedId===null?"active":""}`} type="button" onClick={()=>setSelectedId(null)} title="Pengaturan halaman">⚙</button><span><strong>{page.title||"Halaman tanpa judul"}</strong><small>{page.id?`Diperbarui ${pageDate(page.updatedAt)}`:"Halaman baru"}</small></span></div>
      <div className="elementor-device-switch" aria-label="Mode responsif">{(["desktop","tablet","mobile"] as BuilderDevice[]).map(item=><button className={device===item?"active":""} type="button" key={item} onClick={()=>setDevice(item)} aria-pressed={device===item}>{item==="desktop"?"▰ Desktop":item==="tablet"?"▯ Tablet":"▯ Mobile"}</button>)}</div>
      <div className="elementor-topbar-actions"><button className="elementor-icon-button" type="button" disabled={!past.length} onClick={undo} title="Undo">↶</button><button className="elementor-icon-button" type="button" disabled={!future.length} onClick={redo} title="Redo">↷</button>{page.id&&page.slug&&<a className="cms-button secondary" href={`/${page.slug}?preview=1`} target="_blank" rel="noreferrer">Preview</a>}<button className="cms-button secondary" type="button" disabled={saving} onClick={()=>void save("draft")}>Simpan draft</button>{user.role==="super_admin"&&<button className="cms-button" type="button" disabled={saving} onClick={()=>void save(page.status)}>{saving&&<span className="button-spinner"/>}{saving?"Menyimpan…":page.status==="published"?"Update":"Simpan"}</button>}</div>
    </header>
    {notice&&<p className={`form-message ${notice.type} elementor-notice`} role="status" aria-live="polite">{notice.message}</p>}
    <div className="elementor-editor">
      <aside className="elementor-widgets">
        <div className="elementor-panel-tabs"><button className={sidebarTab==="widgets"?"active":""} type="button" onClick={()=>setSidebarTab("widgets")}>Widget</button><button className={sidebarTab==="navigator"?"active":""} type="button" onClick={()=>setSidebarTab("navigator")}>Navigator</button></div>
        {sidebarTab==="widgets"?<><label className="elementor-widget-search">Cari widget<input className="cms-input" type="search" value={widgetSearch} onChange={event=>setWidgetSearch(event.target.value)} placeholder="Hero, CTA, FAQ…"/></label><p className="elementor-panel-help">Tarik widget ke canvas atau klik untuk menambahkannya.</p><div className="elementor-widget-grid">{allowedWidgets.map(type=><button type="button" draggable key={type} onDragStart={event=>event.dataTransfer.setData("application/x-page-widget",type)} onClick={()=>add(type)}><Icon name={type==="Formulir"?"phone":type==="Portofolio"||type==="Galeri"?"image":type==="CTA"?"arrow":"plus"}/><span>{type}</span></button>)}</div></>:<div className="elementor-navigator">{pageBlocks.map((block,index)=><button className={selectedId===block.id?"active":""} type="button" key={block.id} onClick={()=>setSelectedId(block.id)}><span>⠿</span><strong>{block.headline||block.type}</strong><small>{block.hidden?"Tersembunyi":`${index+1}`}</small></button>)}{!pageBlocks.length&&<p>Belum ada widget.</p>}</div>}
      </aside>
      <main className="elementor-canvas" onDragOver={event=>event.preventDefault()} onDrop={event=>{const type=event.dataTransfer.getData("application/x-page-widget");if(type)add(type)}}>
        <div className={`elementor-stage device-${device}`}>
          <div className="elementor-document"><div className="elementor-document-label"><span>{page.title||"Halaman baru"}</span><button type="button" onClick={()=>setSelectedId(null)}>Edit halaman</button></div>{pageBlocks.map((block,index)=><article className={`elementor-section ${selectedId===block.id?"selected":""} ${block.hidden?"is-hidden":""} ${device==="tablet"&&block.hideTablet||device==="mobile"&&block.hideMobile?"device-hidden":""}`} key={block.id} draggable onDragStart={event=>{event.stopPropagation();setDragged(index)}} onDragOver={event=>event.preventDefault()} onDrop={event=>{event.stopPropagation();drop(index)}} onClick={()=>setSelectedId(block.id)}><div className="elementor-section-handle"><span>⠿ {block.type}</span><div><button type="button" disabled={index===0} onClick={event=>{event.stopPropagation();move(index,-1)}} title="Naik">↑</button><button type="button" disabled={index===pageBlocks.length-1} onClick={event=>{event.stopPropagation();move(index,1)}} title="Turun">↓</button><button type="button" onClick={event=>{event.stopPropagation();duplicate(block,index)}} title="Salin">⧉</button><button type="button" onClick={event=>{event.stopPropagation();applyBlocks(pageBlocks.map(item=>item.id===block.id?{...item,hidden:!item.hidden}:item))}} title={block.hidden?"Tampilkan":"Sembunyikan"}>{block.hidden?"◉":"◌"}</button><button type="button" onClick={event=>{event.stopPropagation();remove(block)}} title="Hapus">×</button></div></div><BlockVisual block={block} device={device}/></article>)}<button className="elementor-drop-zone" type="button" onClick={()=>setSidebarTab("widgets")}>＋ Tarik widget ke sini</button></div>
        </div>
      </main>
      <aside className="elementor-inspector">
        {selected?<><header><button type="button" onClick={()=>setSelectedId(null)}>←</button><span><strong>Edit {selected.type}</strong><small>{selected.hidden?"Tidak tampil di website":"Tampil di website"}</small></span></header><div className="elementor-panel-tabs inspector-tabs">{(["content","style","advanced"] as BuilderInspectorTab[]).map(tab=><button className={inspectorTab===tab?"active":""} type="button" key={tab} onClick={()=>setInspectorTab(tab)}>{tab==="content"?"Konten":tab==="style"?"Gaya":"Lanjutan"}</button>)}</div>{inspectorTab==="content"&&<div className="elementor-controls"><label>Judul section<input className="cms-input" maxLength={300} value={selected.headline} onChange={event=>updateSelected({headline:event.target.value})}/></label><label>Deskripsi<textarea className="cms-input" rows={6} maxLength={2000} value={selected.text} onChange={event=>updateSelected({text:event.target.value})}/></label><p>Perubahan langsung terlihat pada canvas.</p></div>}{inspectorTab==="style"&&<div className="elementor-controls"><label>Warna latar<span className="elementor-color-control"><input type="color" value={selected.background} onChange={event=>updateSelected({background:event.target.value.toUpperCase()})}/><input className="cms-input" value={selected.background} maxLength={7} onChange={event=>/^#[0-9a-fA-F]{0,6}$/.test(event.target.value)&&updateSelected({background:event.target.value.toUpperCase()})}/></span></label><label>Warna teks<span className="elementor-color-control"><input type="color" value={selected.textColor} onChange={event=>updateSelected({textColor:event.target.value.toUpperCase()})}/><input className="cms-input" value={selected.textColor} maxLength={7} onChange={event=>/^#[0-9a-fA-F]{0,6}$/.test(event.target.value)&&updateSelected({textColor:event.target.value.toUpperCase()})}/></span></label><label>Perataan<div className="elementor-alignment">{(["left","center","right"] as const).map(align=><button className={selected.align===align?"active":""} type="button" key={align} onClick={()=>updateSelected({align})}>{align==="left"?"Kiri":align==="center"?"Tengah":"Kanan"}</button>)}</div></label><label>Radius sudut <small>{selected.radius}px</small><input type="range" min="0" max="48" value={selected.radius} onChange={event=>updateSelected({radius:Number(event.target.value)})}/></label></div>}{inspectorTab==="advanced"&&<div className="elementor-controls"><label>Padding desktop <small>{selected.paddingDesktop}px</small><input type="range" min="0" max="160" value={selected.paddingDesktop} onChange={event=>updateSelected({paddingDesktop:Number(event.target.value)})}/></label><label>Padding tablet <small>{selected.paddingTablet}px</small><input type="range" min="0" max="160" value={selected.paddingTablet} onChange={event=>updateSelected({paddingTablet:Number(event.target.value)})}/></label><label>Padding mobile <small>{selected.paddingMobile}px</small><input type="range" min="0" max="160" value={selected.paddingMobile} onChange={event=>updateSelected({paddingMobile:Number(event.target.value)})}/></label><label>ID anchor<input className="cms-input" value={selected.anchor} maxLength={80} onChange={event=>updateSelected({anchor:articleSlug(event.target.value)})} placeholder="contoh: harga"/></label><label className="check-label"><input type="checkbox" checked={selected.hideTablet} onChange={event=>updateSelected({hideTablet:event.target.checked})}/><span>Sembunyikan di tablet</span></label><label className="check-label"><input type="checkbox" checked={selected.hideMobile} onChange={event=>updateSelected({hideMobile:event.target.checked})}/><span>Sembunyikan di mobile</span></label><label className="check-label"><input type="checkbox" checked={selected.hidden} onChange={event=>updateSelected({hidden:event.target.checked})}/><span>Sembunyikan dari website</span></label><div className="elementor-danger-actions"><button type="button" onClick={()=>duplicate(selected,pageBlocks.findIndex(block=>block.id===selected.id))}>Salin widget</button><button type="button" onClick={()=>remove(selected)}>Hapus widget</button></div></div>}</>:<><header><span><strong>Pengaturan halaman</strong><small>Identitas, publikasi, dan SEO</small></span></header><div className="elementor-controls"><label>Judul halaman<input className="cms-input" required maxLength={180} value={page.title} onChange={event=>change({title:event.target.value,...(!page.id?{slug:articleSlug(event.target.value)}:{})})}/></label><label>Slug halaman<input className="cms-input" required maxLength={180} value={page.slug} onChange={event=>change({slug:articleSlug(event.target.value)})}/></label><label>Template<select className="cms-input" disabled={user.role==="sales_promotion"} value={page.template} onChange={event=>change({template:event.target.value})}><option value="default">Default</option><option value="promotion">Landing promo</option><option value="full-width">Lebar penuh</option></select></label>{user.role==="super_admin"&&<label>Status<select className="cms-input" value={page.status} onChange={event=>change({status:event.target.value})}><option value="draft">Draft</option><option value="scheduled">Terjadwal</option><option value="published">Diterbitkan</option><option value="archived">Diarsipkan</option></select></label>}{page.status==="scheduled"&&<label>Jadwal publikasi<input className="cms-input" type="datetime-local" value={pageDatetimeInput(page.scheduledAt)} onChange={event=>change({scheduledAt:event.target.value?new Date(event.target.value).toISOString():""})}/></label>}<hr/><label>SEO title <small>{(page.seoTitle||page.title).length}/60</small><input className="cms-input" maxLength={220} value={page.seoTitle} onChange={event=>change({seoTitle:event.target.value})}/></label><label>Meta description <small>{page.metaDescription.length}/160</small><textarea className="cms-input" rows={4} maxLength={320} value={page.metaDescription} onChange={event=>change({metaDescription:event.target.value})}/></label><label>Canonical URL<input className="cms-input" maxLength={500} value={page.canonicalUrl} onChange={event=>change({canonicalUrl:event.target.value})} placeholder={`/${page.slug||"slug-halaman"}`}/></label><label className="check-label"><input type="checkbox" checked={page.robotsIndex} onChange={event=>change({robotsIndex:event.target.checked})}/><span>Index di Google</span></label><label className="check-label"><input type="checkbox" checked={page.robotsFollow} onChange={event=>change({robotsFollow:event.target.checked})}/><span>Ikuti tautan</span></label><div className={`area-seo-summary ${seo.className}`}><span className="seo-score-dot"/><div><strong>{seo.label} · {seo.passed}/{seo.total}</strong><small>Judul, slug, meta, dan widget aktif.</small></div></div><p className="builder-safety-note">Widget tervalidasi; JavaScript dan HTML berbahaya diblokir.</p>{page.id&&user.role==="super_admin"&&<section className="page-revisions"><h3>Riwayat versi</h3>{page.versions?.length?<div>{page.versions.map(version=><article key={version.id}><span><strong>Versi {version.version}</strong><small>{pageDate(version.createdAt)} · {version.authorName||"Admin"}</small></span><button className="cms-button secondary" type="button" disabled={restoringId!==null} onClick={()=>void restore(version)}>{restoringId===version.id?"Memulihkan…":"Pulihkan"}</button></article>)}</div>:<p>Versi muncul setelah halaman diperbarui.</p>}</section>}</div></>}
      </aside>
    </div>
  </div>
}

function articleSlug(value:string){return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"").slice(0,180)}
function emptyArticle(user:CmsUser):ArticleRecord{return{id:"",title:"",slug:"",excerpt:"",contentBody:"",featuredImageId:null,categoryIds:[],tagIds:[],status:"draft",focusKeyword:"",seoTitle:"",metaDescription:"",canonicalUrl:"",robotsIndex:true,robotsFollow:true,scheduledAt:"",publishedAt:"",readingMinutes:1,authorName:user.name,authorId:user.id,updatedAt:""}}
const articleStatusLabel:Record<string,string>={draft:"Draft",review:"Menunggu review",scheduled:"Terjadwal",published:"Diterbitkan",archived:"Diarsipkan"};

type SeoCheck={label:string;passed:boolean;detail:string};
function articleSeoChecks(article:ArticleRecord,media:MediaRecord[]):SeoCheck[]{
  const focus=article.focusKeyword.trim().toLowerCase();
  const title=(article.seoTitle.trim()||article.title.trim());
  const text=article.contentBody.replace(/\[\[image:[^\]]+\]\]/g," ");
  const wordCount=text.trim().split(/\s+/).filter(Boolean).length;
  const featured=media.find(item=>item.id===article.featuredImageId);
  return[
    {label:"Fokus keyword",passed:focus.length>=3,detail:"Isi kata kunci utama yang benar-benar dibahas."},
    {label:"Keyword dalam judul",passed:Boolean(focus)&&article.title.toLowerCase().includes(focus),detail:"Gunakan fokus keyword secara alami di judul artikel."},
    {label:"Keyword dalam ringkasan",passed:Boolean(focus)&&article.excerpt.toLowerCase().includes(focus),detail:"Sertakan keyword pada ringkasan tanpa mengulang berlebihan."},
    {label:"Panjang SEO title",passed:title.length>=30&&title.length<=60,detail:`${title.length}/60 karakter; target ideal 30–60.`},
    {label:"Panjang meta description",passed:article.metaDescription.length>=120&&article.metaDescription.length<=160,detail:`${article.metaDescription.length}/160 karakter; target ideal 120–160.`},
    {label:"Panjang isi artikel",passed:wordCount>=300,detail:`${wordCount} kata; target awal minimal 300 kata.`},
    {label:"Struktur heading",passed:/^#{2,3}\s+\S/m.test(article.contentBody),detail:"Tambahkan minimal satu heading dengan ## atau ###."},
    {label:"Featured image dan alt text",passed:Boolean(featured&&featured.altText.trim()),detail:"Pilih gambar utama yang memiliki alt text deskriptif."},
    {label:"Kategori artikel",passed:article.categoryIds.length>0,detail:"Pilih minimal satu kategori yang relevan."},
    {label:"Slug URL",passed:article.slug.length>=3&&article.slug.length<=75&&!/--/.test(article.slug),detail:`${article.slug.length} karakter; gunakan slug singkat dan mudah dibaca.`},
  ];
}
function SeoIndicator({article,media}:{article:ArticleRecord;media:MediaRecord[]}){
  const checks=articleSeoChecks(article,media),passed=checks.filter(check=>check.passed).length,score=Math.round(passed/checks.length*100);
  const tone=score>=75?"good":score>=45?"warning":"poor",label=tone==="good"?"SEO baik":tone==="warning"?"Perlu perbaikan":"SEO masih lemah";
  return <section className={`seo-indicator seo-${tone} span-2`} aria-label="Analisis SEO artikel"><div className="seo-score-row"><span className="seo-score-dot" aria-hidden="true"/><div><strong>{label}</strong><small>{passed} dari {checks.length} pemeriksaan lulus</small></div><b>{score}</b></div><div className="seo-progress" aria-label={`Skor SEO ${score} dari 100`}><i style={{width:`${score}%`}}/></div><ul className="seo-check-list">{checks.map(check=><li className={check.passed?"passed":"needs-work"} key={check.label}><span aria-hidden="true">{check.passed?"✓":"!"}</span><div><strong>{check.label}</strong><small>{check.detail}</small></div></li>)}</ul></section>
}

function editorEscape(value:string){return value.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
function editorInline(value:string){
  return editorEscape(value)
    .replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^)\s]+)\)/g,'<a href="$2">$1</a>')
    .replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>")
    .replace(/~~([^~]+)~~/g,"<s>$1</s>")
    .replace(/\+\+([^+]+)\+\+/g,"<u>$1</u>")
    .replace(/`([^`]+)`/g,"<code>$1</code>")
    .replace(/_([^_]+)_/g,"<em>$1</em>");
}
function markdownToEditorHtml(value:string){
  const lines=value.replace(/\r\n/g,"\n").split("\n"),html:string[]=[];
  for(let index=0;index<lines.length;){
    const line=lines[index].trim();
    if(!line){index++;continue}
    const image=line.match(/^\[\[image:([a-zA-Z0-9_-]+)\|([^\]]+)\]\]$/);
    if(image){const alt=editorEscape(image[2].trim());html.push(`<figure contenteditable="false" data-media-id="${editorEscape(image[1])}" data-alt="${alt}"><img src="/api/cms/media/${editorEscape(image[1])}" alt="${alt}"><figcaption>${alt}</figcaption></figure>`);index++;continue}
    if(line==="---"){html.push("<hr>");index++;continue}
    if(line.startsWith("```")){const code:string[]=[];index++;while(index<lines.length&&!lines[index].trim().startsWith("```")){code.push(lines[index]);index++}if(index<lines.length)index++;html.push(`<pre>${editorEscape(code.join("\n"))}</pre>`);continue}
    if(/^\|.+\|$/.test(line)&&index+1<lines.length&&/^\|(?:\s*:?-+:?\s*\|)+$/.test(lines[index+1].trim())){const rows:string[][]=[];rows.push(line.slice(1,-1).split("|").map(cell=>cell.trim()));index+=2;while(index<lines.length&&/^\|.+\|$/.test(lines[index].trim())){rows.push(lines[index].trim().slice(1,-1).split("|").map(cell=>cell.trim()));index++}html.push(`<table><thead><tr>${rows[0].map(cell=>`<th>${editorInline(cell)}</th>`).join("")}</tr></thead><tbody>${rows.slice(1).map(row=>`<tr>${row.map(cell=>`<td>${editorInline(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);continue}
    const aligned=line.match(/^\[(left|center|right|justify)\](.*)\[\/\1\]$/);if(aligned){html.push(`<p data-align="${aligned[1]}" style="text-align:${aligned[1]}">${editorInline(aligned[2])}</p>`);index++;continue}
    if(line.startsWith("###### ")){html.push(`<h6>${editorInline(line.slice(7))}</h6>`);index++;continue}
    if(line.startsWith("##### ")){html.push(`<h5>${editorInline(line.slice(6))}</h5>`);index++;continue}
    if(line.startsWith("#### ")){html.push(`<h4>${editorInline(line.slice(5))}</h4>`);index++;continue}
    if(line.startsWith("### ")){html.push(`<h3>${editorInline(line.slice(4))}</h3>`);index++;continue}
    if(line.startsWith("## ")){html.push(`<h2>${editorInline(line.slice(3))}</h2>`);index++;continue}
    if(line.startsWith("- ")){const items:string[]=[];while(index<lines.length&&lines[index].trim().startsWith("- ")){items.push(`<li>${editorInline(lines[index].trim().slice(2))}</li>`);index++}html.push(`<ul>${items.join("")}</ul>`);continue}
    if(/^\d+\.\s/.test(line)){const items:string[]=[];while(index<lines.length&&/^\d+\.\s/.test(lines[index].trim())){items.push(`<li>${editorInline(lines[index].trim().replace(/^\d+\.\s/,""))}</li>`);index++}html.push(`<ol>${items.join("")}</ol>`);continue}
    if(line.startsWith("> ")){html.push(`<blockquote>${editorInline(line.slice(2))}</blockquote>`);index++;continue}
    html.push(`<p>${editorInline(line)}</p>`);index++;
  }
  return html.join("")||"<p><br></p>";
}
function editorInlineToMarkdown(node:Node):string{
  if(node.nodeType===Node.TEXT_NODE)return node.textContent??"";
  if(!(node instanceof HTMLElement))return"";
  const content=Array.from(node.childNodes).map(editorInlineToMarkdown).join("");
  if(node.tagName==="STRONG"||node.tagName==="B")return`**${content}**`;
  if(node.tagName==="EM"||node.tagName==="I")return`_${content}_`;
  if(node.tagName==="U")return`++${content}++`;
  if(node.tagName==="S"||node.tagName==="STRIKE"||node.tagName==="DEL")return`~~${content}~~`;
  if(node.tagName==="CODE")return`\`${content.replace(/`/g,"'")}\``;
  if(node.tagName==="A"){const href=node.getAttribute("href")??"";return /^(https?:\/\/|\/)/.test(href)?`[${content}](${href})`:content}
  if(node.tagName==="BR")return"\n";
  return content;
}
function editorHtmlToMarkdown(root:HTMLElement){
  const blocks:string[]=[];
  for(const node of Array.from(root.childNodes)){
    if(node.nodeType===Node.TEXT_NODE){const value=node.textContent?.trim();if(value)blocks.push(value);continue}
    if(!(node instanceof HTMLElement))continue;
    if(node.tagName==="FIGURE"){const id=node.dataset.mediaId??"",alt=node.dataset.alt??"Foto artikel";if(id)blocks.push(`[[image:${id}|${alt.replace(/[\[\]|]/g," ")}]]`);continue}
    if(node.tagName==="HR"){blocks.push("---");continue}
    if(node.tagName==="PRE"){const fence=String.fromCharCode(96,96,96);blocks.push(`${fence}\n${node.textContent?.split(fence).join("'''")??""}\n${fence}`);continue}
    if(node.tagName==="TABLE"){const rows=Array.from(node.querySelectorAll("tr")).map(row=>Array.from(row.children).map(cell=>editorInlineToMarkdown(cell).replace(/\|/g,"\\|").trim()));if(rows.length){blocks.push(`| ${rows[0].join(" | ")} |\n| ${rows[0].map(()=>"---").join(" | ")} |${rows.slice(1).map(row=>`\n| ${row.join(" | ")} |`).join("")}`)}continue}
    if(/^H[4-6]$/.test(node.tagName)){blocks.push(`${"#".repeat(Number(node.tagName.slice(1)))} ${editorInlineToMarkdown(node)}`);continue}
    if(node.tagName==="H2"){blocks.push(`## ${editorInlineToMarkdown(node)}`);continue}
    if(node.tagName==="H3"){blocks.push(`### ${editorInlineToMarkdown(node)}`);continue}
    if(node.tagName==="BLOCKQUOTE"){blocks.push(`> ${editorInlineToMarkdown(node)}`);continue}
    if(node.tagName==="UL"||node.tagName==="OL"){Array.from(node.children).forEach((item,index)=>blocks.push(`${node.tagName==="UL"?"-":`${index+1}.`} ${editorInlineToMarkdown(item)}`));continue}
    const value=editorInlineToMarkdown(node).trim();if(value){const align=node.dataset.align||node.style.textAlign;blocks.push(align&&align!=="left"?`[${align}]${value}[/${align}]`:value)}
  }
  return blocks.join("\n\n").slice(0,30000);
}
function EditorTool({label,onAction,children,active=false,disabled=false}:{label:string;onAction:()=>void;children:string;active?:boolean;disabled?:boolean}){return <button className={active?"active":""} type="button" title={label} aria-label={label} aria-pressed={active} disabled={disabled} onMouseDown={event=>{event.preventDefault();onAction()}}>{children}</button>}
type EditorSaveState={tone:"new"|"dirty"|"saving"|"saved"|"error";label:string};
function RichTextEditor({value,onChange,media,onMediaUploaded,saveState,onRequestSave}:{value:string;onChange:(value:string)=>void;media:MediaRecord[];onMediaUploaded:(media:MediaRecord)=>void;saveState:EditorSaveState;onRequestSave:()=>void}){
  const editorRef=useRef<HTMLDivElement>(null),lastEmitted=useRef(""),selectionRef=useRef<Range|null>(null);
  const [mode,setMode]=useState<"visual"|"code">("visual"),[fullscreen,setFullscreen]=useState(false),[mediaOpen,setMediaOpen]=useState(false),[selectedMediaId,setSelectedMediaId]=useState(""),[uploading,setUploading]=useState(false),[mediaNotice,setMediaNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null),[active,setActive]=useState<Set<string>>(new Set()),[block,setBlock]=useState("P");
  useEffect(()=>{if(mode==="visual"&&editorRef.current&&value!==lastEmitted.current){editorRef.current.innerHTML=markdownToEditorHtml(value);lastEmitted.current=value}},[value,mode]);
  function emit(){if(!editorRef.current)return;const markdown=editorHtmlToMarkdown(editorRef.current);lastEmitted.current=markdown;onChange(markdown);refreshToolbar()}
  function saveSelection(){const selection=window.getSelection();if(selection?.rangeCount&&editorRef.current?.contains(selection.anchorNode))selectionRef.current=selection.getRangeAt(0).cloneRange()}
  function restoreSelection(){const range=selectionRef.current,selection=window.getSelection();if(range&&selection){selection.removeAllRanges();selection.addRange(range)}editorRef.current?.focus()}
  function refreshToolbar(){const next=new Set<string>();for(const name of ["bold","italic","underline","strikeThrough","insertUnorderedList","insertOrderedList","justifyLeft","justifyCenter","justifyRight","justifyFull"]){try{if(document.queryCommandState(name))next.add(name)}catch{}}setActive(next);const selection=window.getSelection(),element=selection?.anchorNode instanceof Element?selection.anchorNode:selection?.anchorNode?.parentElement;setBlock(element?.closest("h2,h3,h4,h5,h6,blockquote,pre,p")?.tagName??"P");saveSelection()}
  function command(name:string,argument?:string){restoreSelection();document.execCommand(name,false,argument);emit()}
  function insertHtml(html:string){restoreSelection();document.execCommand("insertHTML",false,html);emit()}
  function addLink(){saveSelection();const url=window.prompt("Masukkan alamat tautan (https://... atau /halaman):")?.trim();if(!url)return;if(!/^(https?:\/\/|\/)/.test(url)){setMediaNotice({type:"error",message:"Tautan harus diawali https:// atau /."});return}command("createLink",url)}
  function insertTable(){saveSelection();const rowInput=window.prompt("Jumlah baris tabel (2–10):","3");if(rowInput===null)return;const columnInput=window.prompt("Jumlah kolom tabel (2–8):","2");if(columnInput===null)return;const rows=Number(rowInput),columns=Number(columnInput);if(!Number.isInteger(rows)||rows<2||rows>10||!Number.isInteger(columns)||columns<2||columns>8){setMediaNotice({type:"error",message:"Jumlah tabel harus berupa angka bulat: 2–10 baris dan 2–8 kolom."});return}const header=`<thead><tr>${Array.from({length:columns},(_,index)=>`<th>Judul ${index+1}</th>`).join("")}</tr></thead>`,body=`<tbody>${Array.from({length:rows-1},()=>`<tr>${Array.from({length:columns},()=>"<td>Isi</td>").join("")}</tr>`).join("")}</tbody>`;insertHtml(`<table>${header}${body}</table><p><br></p>`)}
  function insertMedia(){const item=media.find(candidate=>candidate.id===selectedMediaId);if(!item)return;const alt=editorEscape((item.altText||item.filename.replace(/\.[^.]+$/g,"")||"Foto artikel").replace(/[\[\]|]/g," "));insertHtml(`<figure contenteditable="false" data-media-id="${editorEscape(item.id)}" data-alt="${alt}"><img src="/api/cms/media/${editorEscape(item.id)}" alt="${alt}"><figcaption>${alt}</figcaption></figure><p><br></p>`);setMediaNotice({type:"success",message:`✓ ${item.filename} berhasil disisipkan tepat di posisi kursor.`});setMediaOpen(false);setSelectedMediaId("")}
  async function uploadMedia(file:File){setUploading(true);setMediaNotice({type:"info",message:"Mengunggah dan memeriksa keamanan gambar…"});try{const payload=new FormData();payload.append("file",file);payload.append("altText",file.name.replace(/\.[^.]+$/g,""));const response=await fetch("/api/cms/media",{method:"POST",body:payload}),json=await response.json() as {message?:string;media?:MediaRecord};if(!response.ok||!json.media)throw new Error(json.message||"Gambar gagal diunggah.");onMediaUploaded(json.media);setSelectedMediaId(json.media.id);setMediaNotice({type:"success",message:`✓ ${json.message} Gambar sudah dipilih; klik Sisipkan ke artikel.`})}catch(error){setMediaNotice({type:"error",message:error instanceof Error?error.message:"Gambar gagal diunggah."})}finally{setUploading(false)}}
  const plain=value.replace(/\[\[image:[^\]]+\]\]/g," ").replace(/```[\s\S]*?```/g," ").replace(/[#>*_+~`\[\]|()-]/g," ").trim(),wordCount=plain?plain.split(/\s+/).filter(Boolean).length:0,characterCount=plain.length,selectedMedia=media.find(item=>item.id===selectedMediaId);
  return <div className={`wysiwyg-editor ${fullscreen?"is-fullscreen":""}`}>
    <div className="wysiwyg-topbar"><button className="wysiwyg-add-media" type="button" onMouseDown={event=>{event.preventDefault();saveSelection();setMediaOpen(current=>!current);setMediaNotice(null)}}>▣ Tambah Media</button><div className="wysiwyg-modes" role="tablist" aria-label="Mode editor"><button className={mode==="visual"?"active":""} type="button" role="tab" aria-selected={mode==="visual"} onClick={()=>setMode("visual")}>Visual</button><button className={mode==="code"?"active":""} type="button" role="tab" aria-selected={mode==="code"} onClick={()=>setMode("code")}>Kode</button></div></div>
    {mediaOpen&&<section className="wysiwyg-media-panel"><div className="wysiwyg-media-panel-head"><div><strong>Tambah media</strong><span>Unggah gambar baru atau pilih dari Media Library.</span></div><button type="button" onClick={()=>setMediaOpen(false)} aria-label="Tutup panel media">×</button></div><div className="wysiwyg-media-actions"><label className={`cms-button secondary ${uploading?"disabled":""}`}>{uploading?<><span className="button-spinner"/> Mengunggah…</>:"＋ Unggah gambar"}<input type="file" accept="image/jpeg,image/png,image/webp" disabled={uploading} onChange={event=>{const file=event.currentTarget.files?.[0];if(file)void uploadMedia(file);event.currentTarget.value=""}}/></label><select className="cms-input" value={selectedMediaId} onChange={event=>setSelectedMediaId(event.target.value)}><option value="">Pilih dari Media Library…</option>{media.map(item=><option value={item.id} key={item.id}>{item.filename}</option>)}</select><button className="cms-button" type="button" disabled={!selectedMedia||uploading} onClick={insertMedia}>Sisipkan ke artikel</button></div>{selectedMedia&&<div className="wysiwyg-media-selected"><Image src={`/api/cms/media/${selectedMedia.id}`} unoptimized width={94} height={70} alt={selectedMedia.altText||selectedMedia.filename}/><span><strong>{selectedMedia.filename}</strong><small>{selectedMedia.altText||"Alt text belum diisi"}</small></span><b>Siap disisipkan</b></div>}{mediaNotice&&<p className={`form-message ${mediaNotice.type}`} role="status" aria-live="polite">{mediaNotice.message}</p>}</section>}
    {mode==="visual"?<><div className="wysiwyg-toolbar" role="toolbar" aria-label="Format isi artikel"><div className="wysiwyg-toolbar-row"><select aria-label="Format paragraf" value={block.toLowerCase()} onChange={event=>command("formatBlock",event.target.value)}><option value="p">Paragraf</option><option value="h2">Judul 2</option><option value="h3">Judul 3</option><option value="h4">Judul 4</option><option value="h5">Judul 5</option><option value="h6">Judul 6</option><option value="blockquote">Kutipan</option><option value="pre">Kode blok</option></select><span aria-hidden="true"/><EditorTool label="Tebal (Ctrl+B)" active={active.has("bold")} onAction={()=>command("bold")}>B</EditorTool><EditorTool label="Miring (Ctrl+I)" active={active.has("italic")} onAction={()=>command("italic")}>I</EditorTool><EditorTool label="Garis bawah (Ctrl+U)" active={active.has("underline")} onAction={()=>command("underline")}>U</EditorTool><EditorTool label="Coret" active={active.has("strikeThrough")} onAction={()=>command("strikeThrough")}>S</EditorTool><span aria-hidden="true"/><EditorTool label="Daftar poin" active={active.has("insertUnorderedList")} onAction={()=>command("insertUnorderedList")}>• List</EditorTool><EditorTool label="Daftar nomor" active={active.has("insertOrderedList")} onAction={()=>command("insertOrderedList")}>1. List</EditorTool><EditorTool label="Kutipan" onAction={()=>command("formatBlock","blockquote")}>❝</EditorTool><EditorTool label="Garis horizontal" onAction={()=>command("insertHorizontalRule")}>―</EditorTool></div><div className="wysiwyg-toolbar-row"><EditorTool label="Rata kiri" active={active.has("justifyLeft")} onAction={()=>command("justifyLeft")}>≡←</EditorTool><EditorTool label="Rata tengah" active={active.has("justifyCenter")} onAction={()=>command("justifyCenter")}>≡</EditorTool><EditorTool label="Rata kanan" active={active.has("justifyRight")} onAction={()=>command("justifyRight")}>→≡</EditorTool><EditorTool label="Rata penuh" active={active.has("justifyFull")} onAction={()=>command("justifyFull")}>☰</EditorTool><span aria-hidden="true"/><EditorTool label="Kurangi inden" onAction={()=>command("outdent")}>⇤</EditorTool><EditorTool label="Tambah inden" onAction={()=>command("indent")}>⇥</EditorTool><EditorTool label="Tambahkan tautan" onAction={addLink}>🔗</EditorTool><EditorTool label="Hapus tautan" onAction={()=>command("unlink")}>⛓</EditorTool><EditorTool label="Sisipkan tabel" onAction={insertTable}>▦</EditorTool><select aria-label="Sisipkan karakter khusus" defaultValue="" onChange={event=>{if(event.target.value)command("insertText",event.target.value);event.target.value=""}}><option value="">Ω Karakter</option><option value="©">© Copyright</option><option value="®">® Registered</option><option value="™">™ Trademark</option><option value="°">° Derajat</option><option value="±">± Plus-minus</option><option value="→">→ Panah</option><option value="✓">✓ Centang</option></select><span aria-hidden="true"/><EditorTool label="Hapus format" onAction={()=>command("removeFormat")}>Tx</EditorTool><EditorTool label="Urungkan (Ctrl+Z)" onAction={()=>command("undo")}>↶</EditorTool><EditorTool label="Ulangi (Ctrl+Y)" onAction={()=>command("redo")}>↷</EditorTool><EditorTool label={fullscreen?"Keluar layar penuh":"Layar penuh"} active={fullscreen} onAction={()=>setFullscreen(current=>!current)}>{fullscreen?"⊙":"⛶"}</EditorTool></div></div><div ref={editorRef} className="wysiwyg-content" contentEditable role="textbox" aria-multiline="true" aria-label="Isi artikel" data-placeholder="Mulai tulis artikel di sini…" suppressContentEditableWarning onInput={emit} onBlur={emit} onMouseUp={refreshToolbar} onKeyUp={refreshToolbar} onKeyDown={event=>{if((event.ctrlKey||event.metaKey)&&event.key.toLowerCase()==="s"){event.preventDefault();emit();onRequestSave()}}} onPaste={event=>{event.preventDefault();document.execCommand("insertText",false,event.clipboardData.getData("text/plain"));emit()}}/></>:<div className="wysiwyg-code-wrap"><div><strong>Mode kode aman</strong><span>Gunakan Markdown; HTML dan JavaScript bebas tidak diterima.</span></div><textarea className="wysiwyg-code" value={value} onChange={event=>onChange(event.target.value.slice(0,30000))} onKeyDown={event=>{if((event.ctrlKey||event.metaKey)&&event.key.toLowerCase()==="s"){event.preventDefault();onRequestSave()}}} spellCheck={false} aria-label="Kode Markdown artikel"/></div>}
    <div className="wysiwyg-footer"><span className="wysiwyg-block-indicator">{block}</span><span>{wordCount} kata</span><span>{characterCount} karakter</span><span>± {Math.max(1,Math.ceil(wordCount/200))} menit baca</span><button type="button" onClick={onRequestSave}>Ctrl+S · Simpan</button><strong className={`editor-save-state ${saveState.tone}`}>{saveState.tone==="saving"&&<span className="button-spinner"/>}{saveState.label}</strong></div>
  </div>
}

function ArticleFields({article,media,categories,tags,user,onChange,onMediaUploaded,onRequestSave,saveState,autoSlug=false}:{article:ArticleRecord;media:MediaRecord[];categories:TaxonomyRecord[];tags:TaxonomyRecord[];user:CmsUser;onChange:(patch:Partial<ArticleRecord>)=>void;onMediaUploaded:(media:MediaRecord)=>void;onRequestSave:()=>void;saveState:EditorSaveState;autoSlug?:boolean}){
  const selectedMedia=media.find(item=>item.id===article.featuredImageId);
  return <div className="article-editor-grid">
    <label>Judul artikel<input className="cms-input" required maxLength={220} value={article.title} onChange={event=>onChange({title:event.target.value,...(autoSlug?{slug:articleSlug(event.target.value)}:{})})} placeholder="Judul yang jelas dan membantu pembaca"/></label>
    <label>Slug URL<input className="cms-input" required maxLength={180} value={article.slug} onChange={event=>onChange({slug:articleSlug(event.target.value)})}/></label>
    <label className="span-2">Ringkasan<textarea className="cms-input" required rows={3} maxLength={600} value={article.excerpt} onChange={event=>onChange({excerpt:event.target.value})} placeholder="Ringkasan singkat yang tampil pada daftar artikel dan hasil pencarian."/></label>
    <div className="span-2 article-body-field"><strong>Isi artikel</strong><small>Editor visual penuh: format teks, heading, daftar, tautan, tabel, media, mode kode aman, dan pintasan Ctrl+S.</small><RichTextEditor value={article.contentBody} media={media} saveState={saveState} onChange={contentBody=>onChange({contentBody})} onMediaUploaded={onMediaUploaded} onRequestSave={onRequestSave}/></div>
    <label>Featured image<select className="cms-input" value={article.featuredImageId??""} onChange={event=>onChange({featuredImageId:event.target.value||null})}><option value="">Tanpa gambar</option>{media.map(item=><option value={item.id} key={item.id}>{item.filename}</option>)}</select></label>
    <label>Status<select className="cms-input" value={article.status} onChange={event=>onChange({status:event.target.value})}>{user.role==="super_admin"?<><option value="draft">Draft</option><option value="review">Menunggu review</option><option value="scheduled">Terjadwal</option><option value="published">Diterbitkan</option><option value="archived">Diarsipkan</option></>:<><option value="draft">Draft</option><option value="review">Kirim untuk review</option></>}</select></label>
    {selectedMedia&&<div className="article-media-preview span-2"><Image src={`/api/cms/media/${selectedMedia.id}`} unoptimized width={120} height={80} alt={selectedMedia.altText||selectedMedia.filename}/><span><strong>Featured image dipilih</strong><small>{selectedMedia.filename}</small></span></div>}
    <section className="article-taxonomy-field"><strong>Kategori</strong><small>Pilih kategori utama artikel.</small>{categories.length?<div className="taxonomy-options">{categories.map(item=><label className="taxonomy-option" key={item.id}><input type="checkbox" checked={article.categoryIds.includes(item.id)} onChange={()=>onChange({categoryIds:article.categoryIds.includes(item.id)?article.categoryIds.filter(id=>id!==item.id):[...article.categoryIds,item.id]})}/><span>{item.name}</span></label>)}</div>:<p>Belum ada kategori. Buat melalui menu Kategori & Tag.</p>}</section>
    <section className="article-taxonomy-field"><strong>Tag</strong><small>Pilih tag yang paling relevan.</small>{tags.length?<div className="taxonomy-options">{tags.map(item=><label className="taxonomy-option" key={item.id}><input type="checkbox" checked={article.tagIds.includes(item.id)} onChange={()=>onChange({tagIds:article.tagIds.includes(item.id)?article.tagIds.filter(id=>id!==item.id):[...article.tagIds,item.id]})}/><span>{item.name}</span></label>)}</div>:<p>Belum ada tag. Buat melalui menu Kategori & Tag.</p>}</section>
    {article.status==="scheduled"&&<label className="span-2">Jadwal publikasi<input className="cms-input" required type="datetime-local" value={article.scheduledAt?.slice(0,16)} onChange={event=>onChange({scheduledAt:event.target.value})}/></label>}
    <label>Fokus keyword<input className="cms-input" maxLength={160} value={article.focusKeyword} onChange={event=>onChange({focusKeyword:event.target.value})} placeholder="Contoh: biaya instalasi listrik Bogor"/></label>
    <label>SEO title <small>{article.seoTitle.length}/60</small><input className="cms-input" maxLength={220} value={article.seoTitle} onChange={event=>onChange({seoTitle:event.target.value})} placeholder="Otomatis menggunakan judul jika kosong"/></label>
    <label className="span-2">Meta description <small>{article.metaDescription.length}/160</small><textarea className="cms-input" rows={3} maxLength={320} value={article.metaDescription} onChange={event=>onChange({metaDescription:event.target.value})} placeholder="Ringkas manfaat artikel untuk hasil pencarian."/></label>
    <SeoIndicator article={article} media={media}/>
    <label className="span-2">Canonical URL<input className="cms-input" maxLength={500} value={article.canonicalUrl} onChange={event=>onChange({canonicalUrl:event.target.value})} placeholder="Otomatis jika dikosongkan"/></label>
    <label className="check-label"><input type="checkbox" checked={article.robotsIndex} onChange={event=>onChange({robotsIndex:event.target.checked})}/><span>Izinkan mesin pencari mengindeks artikel</span></label>
    <label className="check-label"><input type="checkbox" checked={article.robotsFollow} onChange={event=>onChange({robotsFollow:event.target.checked})}/><span>Izinkan mesin pencari mengikuti tautan</span></label>
  </div>
}

function ArticleManager({user,mode,onNavigate}:{user:CmsUser;mode:"create"|"list";onNavigate:(view:string)=>void}){
  const [items,setItems]=useState<ArticleRecord[]>([]);
  const [media,setMedia]=useState<MediaRecord[]>([]);
  const [categories,setCategories]=useState<TaxonomyRecord[]>([]);
  const [tags,setTags]=useState<TaxonomyRecord[]>([]);
  const [draft,setDraft]=useState<ArticleRecord>(()=>emptyArticle(user));
  const [openId,setOpenId]=useState<string|null>(null);
  const [loading,setLoading]=useState(true);
  const [savingId,setSavingId]=useState<string|null>(null);
  const [draftDirty,setDraftDirty]=useState(false);
  const [dirtyIds,setDirtyIds]=useState<Set<string>>(()=>new Set());
  const [notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{
    Promise.all([fetch("/api/cms/articles",{cache:"no-store"}),fetch("/api/cms/media",{cache:"no-store"}),fetch("/api/cms/taxonomies",{cache:"no-store"})])
      .then(async([articleResponse,mediaResponse,taxonomyResponse])=>{const articleJson=await articleResponse.json() as {articles?:ArticleRecord[];message?:string},mediaJson=await mediaResponse.json() as {media?:MediaRecord[];message?:string},taxonomyJson=await taxonomyResponse.json() as {categories?:TaxonomyRecord[];tags?:TaxonomyRecord[];message?:string};if(!articleResponse.ok)throw new Error(articleJson.message||"Artikel gagal dimuat.");if(!mediaResponse.ok)throw new Error(mediaJson.message||"Media gagal dimuat.");if(!taxonomyResponse.ok)throw new Error(taxonomyJson.message||"Kategori dan tag gagal dimuat.");setItems((articleJson.articles??[]).map(item=>({...item,categoryIds:item.categoryIds??[],tagIds:item.tagIds??[]})));setMedia(mediaJson.media??[]);setCategories(taxonomyJson.categories??[]);setTags(taxonomyJson.tags??[])})
      .catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Artikel gagal dimuat."}))
      .finally(()=>setLoading(false));
  },[]);

  function updateItem(id:string,patch:Partial<ArticleRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item));setDirtyIds(current=>new Set(current).add(id))}
  function updateDraft(patch:Partial<ArticleRecord>){setDraft(current=>({...current,...patch}));setDraftDirty(true)}
  function addMedia(item:MediaRecord){setMedia(current=>[item,...current.filter(existing=>existing.id!==item.id)])}
  function savedTime(value:string){if(!value)return"Belum pernah disimpan";const date=new Date(value);return Number.isNaN(date.getTime())?"Tersimpan":`Tersimpan ${date.toLocaleTimeString("id-ID",{hour:"2-digit",minute:"2-digit",second:"2-digit"})}`}
  function editorState(article:ArticleRecord,isNew=false):EditorSaveState{const key=isNew?"new":article.id;if(savingId===key)return{tone:"saving",label:"Menyimpan perubahan…"};if(isNew)return draftDirty?{tone:"dirty",label:"Perubahan belum disimpan"}:{tone:"new",label:"Draft baru · belum disimpan"};return dirtyIds.has(article.id)?{tone:"dirty",label:"Perubahan belum disimpan"}:{tone:"saved",label:`✓ ${savedTime(article.updatedAt)}`}}
  function saveAction(article:ArticleRecord){if(savingId!==null)return"Menyimpan…";if(article.status==="published")return article.publishedAt?"Perbarui artikel":"Terbitkan artikel";if(article.status==="scheduled")return"Jadwalkan artikel";if(article.status==="review")return"Kirim untuk review";return"Simpan draft"}
  async function save(article:ArticleRecord,isNew=false){
    setSavingId(isNew?"new":article.id);setNotice({type:"info",message:"Menyimpan artikel…"});
    try{const response=await fetch("/api/cms/articles",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(article)});const json=await response.json() as {message?:string;article?:ArticleRecord};if(!response.ok||!json.article)throw new Error(json.message||"Artikel gagal disimpan.");if(isNew){setItems(current=>[json.article!,...current]);setDraft(emptyArticle(user));setDraftDirty(false);setNotice({type:"success",message:`✓ ${json.message} Artikel dapat dibuka melalui menu Semua Artikel.`})}else{setItems(current=>current.map(item=>item.id===json.article?.id?json.article!:item));setDirtyIds(current=>{const next=new Set(current);next.delete(article.id);return next});setNotice({type:"success",message:`✓ ${json.message} Waktu simpan dan status editor sudah diperbarui.`})}}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Artikel gagal disimpan."})}finally{setSavingId(null)}
  }

  return <section className="cms-panel article-manager">
    <div className="cms-panel-header"><div><h2>{mode==="create"?"Buat artikel baru":"Semua artikel"}</h2><p>{mode==="create"?"Tulis artikel, tambahkan foto, periksa SEO, lalu simpan sebagai draft atau terbitkan.":"Buka artikel untuk mengubah isi, gambar, status publikasi, kategori, tag, dan pengaturan SEO."}</p></div><button className="cms-button secondary" type="button" onClick={()=>onNavigate(mode==="create"?"articles-list":"articles-create")}>{mode==="create"?"Lihat semua artikel":"＋ Tambah artikel"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    {mode==="create"?<form className="article-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>Artikel baru</strong><span>Isi konten utama dan pengaturan SEO sebelum menyimpan.</span></div><span className={`editor-state-badge ${editorState(draft,true).tone}`}>{editorState(draft,true).label}</span></div><ArticleFields article={draft} media={media} categories={categories} tags={tags} user={user} autoSlug saveState={editorState(draft,true)} onChange={updateDraft} onMediaUploaded={addMedia} onRequestSave={()=>void save(draft,true)}/><div className="article-editor-actions"><span className="article-save-hint">Ctrl+S juga dapat digunakan dari dalam editor.</span><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>onNavigate("articles-list")}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null}>{savingId==="new"&&<span className="button-spinner"/>}{saveAction(draft)}</button></div></form>:loading?<div className="lead-loading"><span className="button-spinner"/> Memuat artikel…</div>:items.length?<div className="article-admin-list">{items.map(article=>{const isOpen=openId===article.id,state=editorState(article);return <article className={`article-admin-item ${isOpen?"open":""}`} key={article.id}><button className="article-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:article.id)}><span className="article-admin-icon"><Icon name="cable"/></span><span><strong>{article.title}</strong><small>{article.authorName} · /artikel/{article.slug}</small></span><i className={`lead-status status-${article.status}`}>{dirtyIds.has(article.id)?"Belum disimpan":articleStatusLabel[article.status]||article.status}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="article-editor" onSubmit={event=>{event.preventDefault();void save(article)}}><div className="article-edit-state-row"><span className={`editor-state-badge ${state.tone}`}>{state.label}</span><small>Status publikasi: {articleStatusLabel[article.status]||article.status}</small></div><ArticleFields article={article} media={media} categories={categories} tags={tags} user={user} saveState={state} onChange={patch=>updateItem(article.id,patch)} onMediaUploaded={addMedia} onRequestSave={()=>void save(article)}/><div className="article-editor-actions"><span className="article-save-hint">{state.label}</span>{article.status==="published"&&<a className="cms-button secondary" href={`/artikel/${article.slug}`} target="_blank" rel="noreferrer">Lihat artikel</a>}<button className="cms-button" type="submit" disabled={savingId!==null}>{savingId===article.id&&<span className="button-spinner"/>}{saveAction(article)}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Belum ada artikel</h2><p>Klik Tambah artikel untuk membuat draft pertama.</p></div>}
  </section>
}

function emptyTaxonomy(type:"category"|"tag"):TaxonomyRecord{return{id:"",type,name:"",slug:"",description:"",robotsIndex:false,usageCount:0}}
export const taxonomyManagerLabel="Kategori & Tag artikel";
function TaxonomyManager({user,view}:{user:CmsUser;view:"category"|"tag"}){
  const [categories,setCategories]=useState<TaxonomyRecord[]>([]),[tags,setTags]=useState<TaxonomyRecord[]>([]),[categoryDraft,setCategoryDraft]=useState<TaxonomyRecord>(()=>emptyTaxonomy("category")),[tagDraft,setTagDraft]=useState<TaxonomyRecord>(()=>emptyTaxonomy("tag")),[loading,setLoading]=useState(true),[savingKey,setSavingKey]=useState<string|null>(null),[confirmDelete,setConfirmDelete]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  const canManage=user.role==="super_admin";
  useEffect(()=>{fetch("/api/cms/taxonomies",{cache:"no-store"}).then(async response=>{const json=await response.json() as {categories?:TaxonomyRecord[];tags?:TaxonomyRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Kategori dan tag gagal dimuat.");setCategories(json.categories??[]);setTags(json.tags??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Kategori dan tag gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(type:"category"|"tag",id:string,patch:Partial<TaxonomyRecord>){const setter=type==="category"?setCategories:setTags;setter(current=>current.map(item=>item.id===id?{...item,...patch}:item))}
  async function save(item:TaxonomyRecord,isNew=false){const key=isNew?`new-${item.type}`:item.id;setSavingKey(key);setNotice({type:"info",message:`Menyimpan ${item.type==="category"?"kategori":"tag"}…`});try{const response=await fetch("/api/cms/taxonomies",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(item)}),json=await response.json() as {message?:string;item?:TaxonomyRecord};if(!response.ok||!json.item)throw new Error(json.message||"Data gagal disimpan.");const setter=item.type==="category"?setCategories:setTags;setter(current=>isNew?[...current,json.item!].sort((a,b)=>a.name.localeCompare(b.name,"id")):current.map(entry=>entry.id===json.item?.id?json.item!:entry));if(isNew){if(item.type==="category")setCategoryDraft(emptyTaxonomy("category"));else setTagDraft(emptyTaxonomy("tag"))}setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Data gagal disimpan."})}finally{setSavingKey(null)}}
  async function remove(item:TaxonomyRecord){setSavingKey(item.id);setNotice({type:"info",message:"Menghapus…"});try{const response=await fetch("/api/cms/taxonomies",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id,type:item.type})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Data gagal dihapus.");if(item.type==="category")setCategories(current=>current.filter(entry=>entry.id!==item.id));else setTags(current=>current.filter(entry=>entry.id!==item.id));setConfirmDelete(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Data gagal dihapus."})}finally{setSavingKey(null)}}
  function column(type:"category"|"tag",items:TaxonomyRecord[],draft:TaxonomyRecord,setDraft:(value:TaxonomyRecord)=>void){const label=type==="category"?"Kategori":"Tag";return <section className="taxonomy-column"><div className="taxonomy-heading"><div><h3>{label}</h3><p>{type==="category"?"Kelompok utama artikel dan halaman arsip SEO.":"Topik pendukung untuk memudahkan pengunjung menemukan artikel terkait."}</p></div><span>{items.length} item</span></div>{canManage&&<form className="taxonomy-create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><label>Nama<input className="cms-input" required maxLength={100} value={draft.name} onChange={event=>setDraft({...draft,name:event.target.value,slug:articleSlug(event.target.value)})} placeholder={type==="category"?"Contoh: Keselamatan Listrik":"Contoh: MCB"}/></label><label>Slug<input className="cms-input" required maxLength={100} value={draft.slug} onChange={event=>setDraft({...draft,slug:articleSlug(event.target.value)})}/></label>{type==="category"&&<label className="span-2">Deskripsi<textarea className="cms-input" rows={2} maxLength={500} value={draft.description} onChange={event=>setDraft({...draft,description:event.target.value})}/></label>}<label className="check-label span-2"><input type="checkbox" checked={draft.robotsIndex} onChange={event=>setDraft({...draft,robotsIndex:event.target.checked})}/><span>Izinkan halaman arsip diindeks Google</span></label><button className="cms-button span-2" type="submit" disabled={savingKey!==null}>{savingKey===`new-${type}`&&<span className="button-spinner"/>}{savingKey===`new-${type}`?"Menyimpan…":`＋ Tambah ${label.toLowerCase()}`}</button></form>}{items.length?<div className="taxonomy-list">{items.map(item=><article className="taxonomy-item" key={item.id}>{canManage?<><div className="taxonomy-item-grid"><label>Nama<input className="cms-input" value={item.name} onChange={event=>update(type,item.id,{name:event.target.value})}/></label><label>Slug<input className="cms-input" value={item.slug} onChange={event=>update(type,item.id,{slug:articleSlug(event.target.value)})}/></label>{type==="category"&&<label className="span-2">Deskripsi<textarea className="cms-input" rows={2} value={item.description} onChange={event=>update(type,item.id,{description:event.target.value})}/></label>}<label className="check-label span-2"><input type="checkbox" checked={item.robotsIndex} onChange={event=>update(type,item.id,{robotsIndex:event.target.checked})}/><span>Index halaman arsip</span></label></div><div className="taxonomy-actions"><span>{item.usageCount} artikel</span>{confirmDelete===item.id?<><button className="cms-button danger" type="button" disabled={savingKey!==null} onClick={()=>void remove(item)}>{savingKey===item.id?"Menghapus…":"Ya, hapus"}</button><button className="cms-button secondary" type="button" onClick={()=>setConfirmDelete(null)}>Batal</button></>:<><button className="cms-button secondary" type="button" disabled={savingKey!==null} onClick={()=>void save(item)}>{savingKey===item.id?"Menyimpan…":"Simpan"}</button><button className="cms-button danger" type="button" disabled={savingKey!==null||item.usageCount>0} title={item.usageCount>0?"Lepaskan dari artikel sebelum menghapus.":"Hapus"} onClick={()=>setConfirmDelete(item.id)}>Hapus</button></>}</div></>:<div className="taxonomy-readonly"><strong>{item.name}</strong><span>/{type==="category"?"kategori":"tag"}/{item.slug} · {item.usageCount} artikel</span></div>}</article>)}</div>:!loading&&<div className="empty-state compact"><p>Belum ada {label.toLowerCase()}.</p></div>}</section>}
  const isCategory=view==="category";
  return <section className="cms-panel taxonomy-manager"><div className="cms-panel-header"><div><h2>{isCategory?"Kategori artikel":"Tags artikel"}</h2><p>{isCategory?"Kelola kelompok utama artikel dan halaman arsip SEO.":"Kelola kata penanda untuk menghubungkan artikel dengan topik yang lebih spesifik."} Penulis dapat memilihnya di editor; hanya Super Admin yang dapat mengubahnya.</p></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat {isCategory?"kategori":"tags"}…</div>:<div className="taxonomy-layout single">{isCategory?column("category",categories,categoryDraft,setCategoryDraft):column("tag",tags,tagDraft,setTagDraft)}</div>}</section>
}

const promotionStatusLabel:Record<string,string>={draft:"Draft",scheduled:"Terjadwal",published:"Diterbitkan",archived:"Diarsipkan"};
function emptyPromotion():PromotionRecord{return{id:"",title:"",slug:"",description:"",bannerMediaId:null,bannerFilename:"",bannerAltText:"",ctaLabel:"Klaim promo",ctaUrl:"",whatsappTemplate:"Halo Padjajaran Elektrik, saya tertarik dengan promo yang sedang aktif.",startsAt:"",endsAt:"",status:"draft",createdBy:null,updatedAt:""}}
function datetimeInputValue(value:string){if(!value)return"";const date=new Date(value);if(Number.isNaN(date.getTime()))return"";return new Date(date.getTime()-date.getTimezoneOffset()*60000).toISOString().slice(0,16)}
function datetimeIso(value:string){if(!value)return"";const date=new Date(value);return Number.isNaN(date.getTime())?"":date.toISOString()}
function promotionDisplayState(item:PromotionRecord){const now=Date.now(),start=item.startsAt?new Date(item.startsAt).getTime():null,end=item.endsAt?new Date(item.endsAt).getTime():null;if(item.status==="archived")return["Diarsipkan","status-archived"];if(item.status==="draft")return["Draft","status-draft"];if(start&&start>now)return["Belum mulai","status-scheduled"];if(end&&end<now)return["Selesai","status-archived"];return["Sedang tayang","status-published"]}

function PromotionFields({promotion,media,autoSlug=false,uploadingMedia,onChange,onOpenMedia,onUploadMedia}:{promotion:PromotionRecord;media:MediaRecord[];autoSlug?:boolean;uploadingMedia:boolean;onChange:(patch:Partial<PromotionRecord>)=>void;onOpenMedia:()=>void;onUploadMedia:(file:File)=>void}){
  const selected=media.find(item=>item.id===promotion.bannerMediaId);
  return <div className="promotion-editor-grid">
    <label>Judul promo<input className="cms-input" required maxLength={220} value={promotion.title} onChange={event=>onChange({title:event.target.value,...(autoSlug?{slug:articleSlug(event.target.value)}:{})})} placeholder="Contoh: Promo Survei Instalasi Rumah"/></label>
    <label>Slug<input className="cms-input" required maxLength={180} value={promotion.slug} onChange={event=>onChange({slug:articleSlug(event.target.value)})} placeholder="promo-survei-instalasi-rumah"/></label>
    <label className="span-2">Deskripsi promo<textarea className="cms-input" required rows={4} maxLength={4000} value={promotion.description} onChange={event=>onChange({description:event.target.value})} placeholder="Jelaskan syarat, cakupan, dan manfaat promo secara jelas. Jangan membuat diskon atau batas waktu yang belum dikonfirmasi."/></label>
    <div className="promotion-media-field span-2"><div><strong>Banner atau foto promo</strong><span>Unggah langsung atau pilih gambar yang sudah tersedia di Media Library.</span></div><div className="promotion-media-controls"><select className="cms-input" value={promotion.bannerMediaId??""} onChange={event=>onChange({bannerMediaId:event.target.value||null})}><option value="">Tanpa gambar</option>{media.map(item=><option value={item.id} key={item.id}>{item.filename}</option>)}</select><label className={`cms-button secondary promotion-upload-button ${uploadingMedia?"disabled":""}`}>{uploadingMedia&&<span className="button-spinner"/>}{uploadingMedia?"Mengunggah…":"Unggah foto"}<input disabled={uploadingMedia} type="file" accept="image/jpeg,image/png,image/webp" onChange={event=>{const file=event.target.files?.[0];if(file)onUploadMedia(file);event.currentTarget.value=""}}/></label><button className="cms-button secondary" type="button" onClick={onOpenMedia}>Media Library</button></div>{selected&&<div className="promotion-media-preview"><Image unoptimized src={`/api/cms/media/${selected.id}`} width={150} height={94} alt={selected.altText||selected.filename}/><span><strong>Gambar dipilih</strong><small>{selected.filename}</small><small>Alt: {selected.altText||"Belum diisi"}</small></span><button className="cms-button secondary" type="button" onClick={()=>onChange({bannerMediaId:null})}>Lepaskan</button></div>}</div>
    <label>Label tombol CTA<input className="cms-input" maxLength={100} value={promotion.ctaLabel} onChange={event=>onChange({ctaLabel:event.target.value})} placeholder="Klaim promo"/></label>
    <label>URL CTA opsional<input className="cms-input" maxLength={500} value={promotion.ctaUrl} onChange={event=>onChange({ctaUrl:event.target.value})} placeholder="/permintaan-survei atau https://..."/></label>
    <label className="span-2">Template pesan WhatsApp<textarea className="cms-input" rows={3} maxLength={1200} value={promotion.whatsappTemplate} onChange={event=>onChange({whatsappTemplate:event.target.value})} placeholder="Pesan otomatis ketika pengunjung menekan tombol promo."/><small>Digunakan jika URL CTA dikosongkan dan nomor WhatsApp bisnis sudah diisi.</small></label>
    <label>Mulai tayang<input className="cms-input" type="datetime-local" value={datetimeInputValue(promotion.startsAt)} onChange={event=>onChange({startsAt:datetimeIso(event.target.value)})}/></label>
    <label>Berakhir tayang<input className="cms-input" type="datetime-local" value={datetimeInputValue(promotion.endsAt)} onChange={event=>onChange({endsAt:datetimeIso(event.target.value)})}/></label>
    <label>Status<select className="cms-input" value={promotion.status} onChange={event=>onChange({status:event.target.value})}><option value="draft">Draft</option><option value="scheduled">Terjadwal</option><option value="published">Diterbitkan</option><option value="archived">Diarsipkan</option></select></label>
    <div className="promotion-schedule-note"><strong>Aturan penayangan</strong><span>Promo diterbitkan hanya tampil selama periodenya. Status Terjadwal akan aktif otomatis saat waktu mulai tercapai.</span></div>
  </div>
}

function PromoManager({onOpenMedia}:{onOpenMedia:()=>void}){
  const [items,setItems]=useState<PromotionRecord[]>([]),[media,setMedia]=useState<MediaRecord[]>([]),[draft,setDraft]=useState<PromotionRecord>(emptyPromotion()),[showCreate,setShowCreate]=useState(false),[openId,setOpenId]=useState<string|null>(null),[loading,setLoading]=useState(true),[savingId,setSavingId]=useState<string|null>(null),[uploadingMedia,setUploadingMedia]=useState(false),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{Promise.all([fetch("/api/cms/promotions",{cache:"no-store"}),fetch("/api/cms/media",{cache:"no-store"})]).then(async([promotionResponse,mediaResponse])=>{const promotionJson=await promotionResponse.json() as {promotions?:PromotionRecord[];message?:string},mediaJson=await mediaResponse.json() as {media?:MediaRecord[];message?:string};if(!promotionResponse.ok)throw new Error(promotionJson.message||"Promo gagal dimuat.");if(!mediaResponse.ok)throw new Error(mediaJson.message||"Media gagal dimuat.");setItems(promotionJson.promotions??[]);setMedia(mediaJson.media??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Promo gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(id:string,patch:Partial<PromotionRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item))}
  async function uploadBanner(file:File,onSelected:(id:string)=>void){setUploadingMedia(true);setNotice({type:"info",message:`Mengunggah ${file.name}…`});try{const form=new FormData();form.append("file",file);form.append("altText",file.name.replace(/\.[^.]+$/g,""));const response=await fetch("/api/cms/media",{method:"POST",body:form}),json=await response.json() as {message?:string;media?:MediaRecord};if(!response.ok||!json.media)throw new Error(json.message||"Gambar promo gagal diunggah.");setMedia(current=>[json.media!,...current]);onSelected(json.media.id);setNotice({type:"success",message:`✓ ${json.message} Gambar sudah dipilih sebagai banner promo.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Gambar promo gagal diunggah."})}finally{setUploadingMedia(false)}}
  async function save(promotion:PromotionRecord,isNew=false){setSavingId(isNew?"new":promotion.id);setNotice({type:"info",message:"Menyimpan promo…"});try{const response=await fetch("/api/cms/promotions",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(promotion)}),json=await response.json() as {message?:string;promotion?:PromotionRecord};if(!response.ok||!json.promotion)throw new Error(json.message||"Promo gagal disimpan.");if(isNew){setItems(current=>[json.promotion!,...current]);setDraft(emptyPromotion());setShowCreate(false);setOpenId(json.promotion.id)}else setItems(current=>current.map(item=>item.id===json.promotion?.id?json.promotion!:item));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Promo gagal disimpan."})}finally{setSavingId(null)}}
  async function remove(promotion:PromotionRecord){setSavingId(promotion.id);setNotice({type:"info",message:"Menghapus promo…"});try{const response=await fetch("/api/cms/promotions",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:promotion.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Promo gagal dihapus.");setItems(current=>current.filter(item=>item.id!==promotion.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Promo gagal dihapus."})}finally{setSavingId(null)}}
  return <section className="cms-panel promotion-manager"><div className="cms-panel-header"><div><h2>Kelola promo dan periode tayang</h2><p>Promo yang diterbitkan otomatis tampil di halaman publik hanya selama periode yang ditentukan.</p></div><div className="promotion-toolbar"><a className="cms-button secondary" href="/promo" target="_blank" rel="noreferrer">Lihat halaman promo</a><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form promo ditutup.":"Form promo baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah promo"}</button></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{showCreate&&<form className="promotion-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>Promo baru</strong><span>Isi materi, banner, CTA, periode, dan status tayang.</span></div><span className="status-badge">Form terbuka</span></div><PromotionFields promotion={draft} media={media} autoSlug uploadingMedia={uploadingMedia} onChange={patch=>setDraft(current=>({...current,...patch}))} onOpenMedia={onOpenMedia} onUploadMedia={file=>void uploadBanner(file,id=>setDraft(current=>({...current,bannerMediaId:id})))}/><div className="promotion-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null||uploadingMedia} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null||uploadingMedia}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan promo"}</button></div></form>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat promo…</div>:items.length?<div className="promotion-admin-list">{items.map(item=>{const isOpen=openId===item.id,[state,stateClass]=promotionDisplayState(item);return <article className={`promotion-admin-item ${isOpen?"open":""}`} key={item.id}><button className="promotion-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:item.id)}>{item.bannerMediaId?<Image unoptimized src={`/api/cms/media/${item.bannerMediaId}`} width={56} height={44} alt={item.bannerAltText||item.title}/>:<span className="promotion-admin-icon"><Icon name="calendar"/></span>}<span><strong>{item.title}</strong><small>{promotionStatusLabel[item.status]||item.status} · /promo#{item.slug}</small></span><i className={`lead-status ${stateClass}`}>{state}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="promotion-editor" onSubmit={event=>{event.preventDefault();void save(item)}}><PromotionFields promotion={item} media={media} uploadingMedia={uploadingMedia} onChange={patch=>update(item.id,patch)} onOpenMedia={onOpenMedia} onUploadMedia={file=>void uploadBanner(file,id=>update(item.id,{bannerMediaId:id}))}/><div className="promotion-editor-actions">{confirmDeleteId===item.id?<div className="faq-delete-confirm" role="alert"><span>Hapus promo ini?</span><button className="cms-button secondary" type="button" disabled={savingId!==null||uploadingMedia} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={savingId!==null||uploadingMedia} onClick={()=>void remove(item)}>{savingId===item.id?"Menghapus…":"Ya, hapus"}</button></div>:<button className="cms-button danger" type="button" disabled={savingId!==null||uploadingMedia} onClick={()=>setConfirmDeleteId(item.id)}>Hapus</button>}<button className="cms-button" type="submit" disabled={savingId!==null||uploadingMedia}>{savingId===item.id&&<span className="button-spinner"/>}{savingId===item.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Belum ada promo</h2><p>Klik Tambah promo untuk membuat penawaran pertama. Promo draft tidak akan tampil di website.</p></div>}</section>
}

const projectStatusLabel:Record<string,string>={draft:"Draft",published:"Diterbitkan",archived:"Diarsipkan"};
function emptyProject():ProjectRecord{return{id:"",title:"",slug:"",projectType:"",area:"",scope:"",description:"",duration:"",projectYear:new Date().getFullYear(),seoTitle:"",metaDescription:"",robotsIndex:true,sortOrder:10,status:"draft",publishedAt:"",updatedAt:"",media:[]}}
function projectSeoScore(project:ProjectRecord){const checks=[project.seoTitle.length>=30&&project.seoTitle.length<=60,project.metaDescription.length>=120&&project.metaDescription.length<=160,project.scope.length>=80,project.media.length>0,project.media.every(item=>item.altText.trim().length>0)],passed=checks.filter(Boolean).length;return{passed,total:checks.length,label:passed===checks.length?"SEO baik":passed>=3?"Perlu dilengkapi":"SEO masih lemah",className:passed===checks.length?"seo-good":passed>=3?"seo-warning":"seo-poor"}}
function addProjectMedia(project:ProjectRecord,media:MediaRecord){if(project.media.some(item=>item.mediaId===media.id)||project.media.length>=20)return project;return{...project,media:[...project.media,{id:`new-${media.id}`,mediaId:media.id,role:"gallery",sortOrder:project.media.length*10,caption:media.caption,filename:media.filename,altText:media.altText}]}}
function updateProjectMedia(project:ProjectRecord,id:string,patch:Partial<ProjectMediaRecord>){let next=project.media.map(item=>item.id===id?{...item,...patch}:item);if((patch.role==="before"||patch.role==="after"))next=next.map(item=>item.id!==id&&item.role===patch.role?{...item,role:"gallery"}:item);return{...project,media:next}}
function moveProjectMedia(project:ProjectRecord,index:number,direction:-1|1){const target=index+direction;if(target<0||target>=project.media.length)return project;const next=[...project.media],[item]=next.splice(index,1);next.splice(target,0,item);return{...project,media:next.map((entry,position)=>({...entry,sortOrder:position*10}))}}

function ProjectFields({project,media,autoSlug=false,uploadingLabel,onChange,onOpenMedia,onUploadFiles}:{project:ProjectRecord;media:MediaRecord[];autoSlug?:boolean;uploadingLabel:string;onChange:(value:ProjectRecord)=>void;onOpenMedia:()=>void;onUploadFiles:(files:File[])=>void}){
  const [pendingMediaId,setPendingMediaId]=useState(""),seo=projectSeoScore(project),available=media.filter(item=>!project.media.some(selected=>selected.mediaId===item.id));
  function addSelected(){const selected=media.find(item=>item.id===pendingMediaId);if(!selected)return;onChange(addProjectMedia(project,selected));setPendingMediaId("")}
  return <div className="project-editor-grid">
    <label>Judul proyek<input className="cms-input" required maxLength={220} value={project.title} onChange={event=>onChange({...project,title:event.target.value,...(autoSlug?{slug:articleSlug(event.target.value),seoTitle:`${event.target.value} | Padjajaran Elektrik`}:{})})} placeholder="Contoh: Penataan Panel Listrik Rumah di Bogor"/></label>
    <label>Slug halaman<input className="cms-input" required maxLength={180} value={project.slug} onChange={event=>onChange({...project,slug:articleSlug(event.target.value)})} placeholder="penataan-panel-listrik-rumah-bogor"/></label>
    <label>Jenis proyek<input className="cms-input" required maxLength={160} value={project.projectType} onChange={event=>onChange({...project,projectType:event.target.value})} placeholder="Instalasi panel listrik"/></label>
    <label>Area proyek<input className="cms-input" required maxLength={180} value={project.area} onChange={event=>onChange({...project,area:event.target.value})} placeholder="Bogor Tengah"/></label>
    <label>Durasi pengerjaan<input className="cms-input" maxLength={100} value={project.duration} onChange={event=>onChange({...project,duration:event.target.value})} placeholder="Contoh: 2 hari"/></label>
    <label>Tahun pengerjaan<input className="cms-input" type="number" min="2000" max={new Date().getFullYear()+1} value={project.projectYear??""} onChange={event=>onChange({...project,projectYear:event.target.value===""?null:Number(event.target.value)})}/></label>
    <label className="span-2">Lingkup pekerjaan <small>{project.scope.length} karakter · disarankan minimal 80</small><textarea className="cms-input" required rows={4} maxLength={3000} value={project.scope} onChange={event=>onChange({...project,scope:event.target.value})} placeholder="Jelaskan pekerjaan yang benar-benar dilakukan, komponen yang ditangani, serta hasil akhirnya."/></label>
    <label className="span-2">Detail pelaksanaan<textarea className="cms-input" rows={5} maxLength={8000} value={project.description} onChange={event=>onChange({...project,description:event.target.value})} placeholder="Tambahkan kondisi awal, proses pemeriksaan, tantangan lokasi, pengujian, dan serah terima tanpa membuat klaim yang tidak dapat diverifikasi."/></label>
    <section className="project-media-field span-2"><div className="project-media-heading"><div><strong>Foto proyek</strong><span>Unggah hingga 20 foto atau pilih dari Media Library. Tentukan foto Sebelum, Sesudah, atau Galeri.</span></div><span>{project.media.length}/20 foto</span></div><div className="project-media-controls"><select className="cms-input" value={pendingMediaId} onChange={event=>setPendingMediaId(event.target.value)}><option value="">Pilih gambar dari Media Library…</option>{available.map(item=><option value={item.id} key={item.id}>{item.filename}</option>)}</select><button className="cms-button secondary" type="button" disabled={!pendingMediaId} onClick={addSelected}>＋ Tambahkan</button><label className={`cms-button secondary project-upload-button ${uploadingLabel?"disabled":""}`}>{uploadingLabel&&<span className="button-spinner"/>}{uploadingLabel||"Unggah beberapa foto"}<input disabled={Boolean(uploadingLabel)} multiple type="file" accept="image/jpeg,image/png,image/webp" onChange={event=>{const files=Array.from(event.target.files??[]);if(files.length)onUploadFiles(files);event.currentTarget.value=""}}/></label><button className="cms-button secondary" type="button" onClick={onOpenMedia}>Media Library</button></div>{project.media.length?<div className="project-media-list">{project.media.map((item,index)=><article key={item.id}><Image unoptimized src={`/api/cms/media/${item.mediaId}`} width={120} height={86} alt={item.altText||item.filename}/><div><strong>{item.filename}</strong><small>Alt: {item.altText||"Belum diisi di Media Library"}</small><select className="cms-input" aria-label={`Jenis foto ${item.filename}`} value={item.role} onChange={event=>onChange(updateProjectMedia(project,item.id,{role:event.target.value}))}><option value="gallery">Galeri</option><option value="before">Sebelum</option><option value="after">Sesudah</option></select><input className="cms-input" maxLength={500} value={item.caption} onChange={event=>onChange(updateProjectMedia(project,item.id,{caption:event.target.value}))} placeholder="Caption foto (opsional)"/></div><div className="project-media-actions"><button type="button" disabled={index===0} onClick={()=>onChange(moveProjectMedia(project,index,-1))} aria-label="Pindahkan foto ke atas">↑</button><button type="button" disabled={index===project.media.length-1} onClick={()=>onChange(moveProjectMedia(project,index,1))} aria-label="Pindahkan foto ke bawah">↓</button><button type="button" className="remove" onClick={()=>onChange({...project,media:project.media.filter(entry=>entry.id!==item.id).map((entry,position)=>({...entry,sortOrder:position*10}))})}>Hapus dari proyek</button></div></article>)}</div>:<div className="project-media-empty"><Icon name="image"/><div><strong>Belum ada foto dipilih</strong><span>Proyek harus memiliki minimal satu foto asli sebelum diterbitkan.</span></div></div>}</section>
    <label>SEO title <small>{project.seoTitle.length}/60</small><input className="cms-input" maxLength={220} value={project.seoTitle} onChange={event=>onChange({...project,seoTitle:event.target.value})} placeholder="Judul yang tampil di Google"/></label>
    <label>Meta description <small>{project.metaDescription.length}/160</small><textarea className="cms-input" rows={3} maxLength={320} value={project.metaDescription} onChange={event=>onChange({...project,metaDescription:event.target.value})} placeholder="Ringkasan proyek untuk hasil pencarian Google."/></label>
    <div className={`area-seo-summary ${seo.className}`}><span className="seo-score-dot"/><div><strong>{seo.label} · {seo.passed}/{seo.total}</strong><small>Memeriksa judul, meta, lingkup, foto, dan alt text.</small></div></div>
    <div className="project-publish-settings"><label>Urutan tampil<input className="cms-input" type="number" min="0" max="9999" value={project.sortOrder} onChange={event=>onChange({...project,sortOrder:Number(event.target.value)})}/></label><label>Status<select className="cms-input" value={project.status} onChange={event=>onChange({...project,status:event.target.value})}><option value="draft">Draft / disembunyikan</option><option value="published">Diterbitkan</option><option value="archived">Diarsipkan</option></select></label><label className="check-label"><input type="checkbox" checked={project.robotsIndex} onChange={event=>onChange({...project,robotsIndex:event.target.checked})}/><span>Izinkan halaman diindeks Google</span></label></div>
  </div>
}

function PortfolioManager({onOpenMedia}:{onOpenMedia:()=>void}){
  const [items,setItems]=useState<ProjectRecord[]>([]),[media,setMedia]=useState<MediaRecord[]>([]),[draft,setDraft]=useState<ProjectRecord>(emptyProject()),[showCreate,setShowCreate]=useState(false),[openId,setOpenId]=useState<string|null>(null),[loading,setLoading]=useState(true),[savingId,setSavingId]=useState<string|null>(null),[uploadingLabel,setUploadingLabel]=useState(""),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{Promise.all([fetch("/api/cms/projects",{cache:"no-store"}),fetch("/api/cms/media",{cache:"no-store"})]).then(async([projectResponse,mediaResponse])=>{const projectJson=await projectResponse.json() as {projects?:ProjectRecord[];message?:string},mediaJson=await mediaResponse.json() as {media?:MediaRecord[];message?:string};if(!projectResponse.ok)throw new Error(projectJson.message||"Portofolio gagal dimuat.");if(!mediaResponse.ok)throw new Error(mediaJson.message||"Media gagal dimuat.");setItems(projectJson.projects??[]);setMedia(mediaJson.media??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Portofolio gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(id:string,value:ProjectRecord){setItems(current=>current.map(item=>item.id===id?value:item))}
  async function uploadFiles(files:File[],onSelected:(media:MediaRecord)=>void){const selected=files.slice(0,Math.max(0,20));if(!selected.length)return;try{for(let index=0;index<selected.length;index++){const file=selected[index];setUploadingLabel(`Mengunggah ${index+1}/${selected.length}…`);setNotice({type:"info",message:`Mengunggah ${file.name} (${index+1} dari ${selected.length})…`});const form=new FormData();form.append("file",file);form.append("altText",file.name.replace(/\.[^.]+$/g,""));const response=await fetch("/api/cms/media",{method:"POST",body:form}),json=await response.json() as {message?:string;media?:MediaRecord};if(!response.ok||!json.media)throw new Error(json.message||`${file.name} gagal diunggah.`);setMedia(current=>current.some(item=>item.id===json.media!.id)?current:[json.media!,...current]);onSelected(json.media)}setNotice({type:"success",message:`✓ ${selected.length} foto berhasil diunggah dan ditambahkan ke proyek.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Foto proyek gagal diunggah."})}finally{setUploadingLabel("")}}
  async function save(project:ProjectRecord,isNew=false){setSavingId(isNew?"new":project.id);setNotice({type:"info",message:"Menyimpan proyek…"});try{const response=await fetch("/api/cms/projects",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(project)}),json=await response.json() as {message?:string;project?:ProjectRecord};if(!response.ok||!json.project)throw new Error(json.message||"Proyek gagal disimpan.");if(isNew){setItems(current=>[...current,json.project!].sort((a,b)=>a.sortOrder-b.sortOrder));setDraft(emptyProject());setShowCreate(false);setOpenId(json.project.id)}else setItems(current=>current.map(item=>item.id===json.project?.id?json.project!:item).sort((a,b)=>a.sortOrder-b.sortOrder));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Proyek gagal disimpan."})}finally{setSavingId(null)}}
  async function remove(project:ProjectRecord){setSavingId(project.id);setNotice({type:"info",message:"Menghapus proyek…"});try{const response=await fetch("/api/cms/projects",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:project.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Proyek gagal dihapus.");setItems(current=>current.filter(item=>item.id!==project.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Proyek gagal dihapus."})}finally{setSavingId(null)}}
  return <section className="cms-panel portfolio-manager"><div className="cms-panel-header"><div><h2>Kelola portofolio proyek</h2><p>Proyek yang diterbitkan otomatis tampil di beranda, halaman portofolio, detail proyek, Page Builder, dan sitemap.</p></div><div className="project-toolbar"><a className="cms-button secondary" href="/portofolio" target="_blank" rel="noreferrer">Lihat portofolio</a><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form proyek ditutup.":"Form proyek baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah proyek"}</button></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{showCreate&&<form className="project-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>Proyek baru</strong><span>Tambahkan informasi proyek asli beserta dokumentasinya.</span></div><span className="status-badge">Form terbuka</span></div><ProjectFields project={draft} media={media} autoSlug uploadingLabel={uploadingLabel} onChange={setDraft} onOpenMedia={onOpenMedia} onUploadFiles={files=>void uploadFiles(files,item=>setDraft(current=>addProjectMedia(current,item)))}/><div className="project-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null||Boolean(uploadingLabel)} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null||Boolean(uploadingLabel)}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan proyek"}</button></div></form>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat portofolio…</div>:items.length?<div className="project-admin-list">{items.map(project=>{const isOpen=openId===project.id,cover=project.media.find(item=>item.role==="after")??project.media[0],seo=projectSeoScore(project);return <article className={`project-admin-item ${isOpen?"open":""}`} key={project.id}><button className="project-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:project.id)}>{cover?<Image unoptimized src={`/api/cms/media/${cover.mediaId}`} width={64} height={52} alt={cover.altText||project.title}/>:<span className="project-admin-icon"><Icon name="image"/></span>}<span><strong>{project.title}</strong><small>{project.area} · {project.media.length} foto · SEO {seo.passed}/{seo.total}</small></span><i className={`lead-status status-${project.status}`}>{projectStatusLabel[project.status]||project.status}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="project-editor" onSubmit={event=>{event.preventDefault();void save(project)}}><ProjectFields project={project} media={media} uploadingLabel={uploadingLabel} onChange={value=>update(project.id,value)} onOpenMedia={onOpenMedia} onUploadFiles={files=>void uploadFiles(files,item=>setItems(current=>current.map(entry=>entry.id===project.id?addProjectMedia(entry,item):entry)))}/><div className="project-editor-actions">{confirmDeleteId===project.id?<div className="faq-delete-confirm" role="alert"><span>Hapus proyek ini?</span><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>void remove(project)}>{savingId===project.id?"Menghapus…":"Ya, hapus"}</button></div>:<button className="cms-button danger" type="button" disabled={savingId!==null||Boolean(uploadingLabel)} onClick={()=>setConfirmDeleteId(project.id)}>Hapus</button>}{project.status==="published"&&<a className="cms-button secondary" href={`/proyek/${project.slug}`} target="_blank" rel="noreferrer">Lihat halaman</a>}<button className="cms-button" type="submit" disabled={savingId!==null||Boolean(uploadingLabel)}>{savingId===project.id&&<span className="button-spinner"/>}{savingId===project.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Belum ada proyek</h2><p>Klik Tambah proyek, unggah foto asli, lengkapi informasi, lalu terbitkan. Proyek draft tidak tampil di website.</p></div>}</section>
}

function emptyTestimonial():TestimonialRecord{return{id:"",name:"",photoMediaId:null,photoFilename:"",photoAltText:"",serviceType:"",area:"",content:"",source:"",rating:5,verified:false,active:false,sortOrder:10,updatedAt:""}}
function testimonialInitials(name:string){return name.split(/\s+/).filter(Boolean).slice(0,2).map(part=>part[0]?.toUpperCase()).join("")||"PE"}
function TestimonialFields({item,media,autoPhotoUpload,uploading,onChange,onOpenMedia,onUploadPhoto}:{item:TestimonialRecord;media:MediaRecord[];autoPhotoUpload:boolean;uploading:boolean;onChange:(value:TestimonialRecord)=>void;onOpenMedia:()=>void;onUploadPhoto:(file:File)=>void}){
  const selected=media.find(entry=>entry.id===item.photoMediaId);
  return <div className="testimonial-editor-grid"><label>Nama pelanggan<input className="cms-input" required maxLength={160} value={item.name} onChange={event=>onChange({...item,name:event.target.value})} placeholder="Nama yang diizinkan pelanggan untuk ditampilkan"/></label><label>Jenis pekerjaan<input className="cms-input" maxLength={180} value={item.serviceType} onChange={event=>onChange({...item,serviceType:event.target.value})} placeholder="Contoh: Penataan panel listrik"/></label><label>Area<input className="cms-input" maxLength={180} value={item.area} onChange={event=>onChange({...item,area:event.target.value})} placeholder="Contoh: Bogor Tengah"/></label><label>Rating<select className="cms-input" value={item.rating??""} onChange={event=>onChange({...item,rating:event.target.value===""?null:Number(event.target.value)})}><option value="">Tanpa rating</option>{[5,4.5,4,3.5,3,2.5,2,1.5,1].map(value=><option value={value} key={value}>{value} dari 5</option>)}</select></label><label className="span-2">Isi testimoni <small>{item.content.length}/3000 karakter</small><textarea className="cms-input" required rows={5} maxLength={3000} value={item.content} onChange={event=>onChange({...item,content:event.target.value})} placeholder="Masukkan ucapan pelanggan apa adanya. Jangan mengubah makna atau membuat testimoni baru."/></label><label className="span-2">Sumber dan bukti izin <small>Wajib sebelum ditandai terverifikasi</small><input className="cms-input" maxLength={500} value={item.source} onChange={event=>onChange({...item,source:event.target.value})} placeholder="Contoh: WhatsApp pelanggan, izin publikasi diterima 24 Agustus 2026"/></label><section className="testimonial-photo-field span-2"><div><strong>Foto pelanggan (opsional)</strong><span>Gunakan hanya jika pelanggan mengizinkan fotonya ditampilkan.</span></div><div className="testimonial-photo-controls"><select className="cms-input" value={item.photoMediaId??""} onChange={event=>onChange({...item,photoMediaId:event.target.value||null})}><option value="">Tanpa foto</option>{media.map(entry=><option value={entry.id} key={entry.id}>{entry.filename}</option>)}</select><label className={`cms-button secondary testimonial-upload-button ${uploading?"disabled":""}`}>{uploading&&<span className="button-spinner"/>}{uploading?"Mengunggah…":"Unggah foto"}<input disabled={uploading||!autoPhotoUpload} type="file" accept="image/jpeg,image/png,image/webp" onChange={event=>{const file=event.target.files?.[0];if(file)onUploadPhoto(file);event.currentTarget.value=""}}/></label><button className="cms-button secondary" type="button" onClick={onOpenMedia}>Media Library</button></div>{selected&&<div className="testimonial-photo-preview"><Image unoptimized src={`/api/cms/media/${selected.id}`} width={110} height={84} alt={selected.altText||selected.filename}/><span><strong>Foto dipilih</strong><small>{selected.filename}</small><small>Alt: {selected.altText||"Belum diisi"}</small></span><button className="cms-button secondary" type="button" onClick={()=>onChange({...item,photoMediaId:null})}>Lepaskan</button></div>}</section><label>Urutan tampil<input className="cms-input" type="number" min="0" max="9999" value={item.sortOrder} onChange={event=>onChange({...item,sortOrder:Number(event.target.value)})}/></label><div className="testimonial-verification"><label className="check-label"><input type="checkbox" checked={item.verified} onChange={event=>onChange({...item,verified:event.target.checked,active:event.target.checked?item.active:false})}/><span>Testimoni asli dan sumbernya sudah diverifikasi</span></label><label className="check-label"><input type="checkbox" disabled={!item.verified} checked={item.active} onChange={event=>onChange({...item,active:event.target.checked})}/><span>Tampilkan di website</span></label>{!item.verified&&<small>Status tampil terkunci sampai testimoni diverifikasi.</small>}</div></div>
}

function TestimonialManager({onOpenMedia}:{onOpenMedia:()=>void}){
  const [items,setItems]=useState<TestimonialRecord[]>([]),[media,setMedia]=useState<MediaRecord[]>([]),[draft,setDraft]=useState<TestimonialRecord>(emptyTestimonial()),[showCreate,setShowCreate]=useState(false),[openId,setOpenId]=useState<string|null>(null),[loading,setLoading]=useState(true),[savingId,setSavingId]=useState<string|null>(null),[uploading,setUploading]=useState(false),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{Promise.all([fetch("/api/cms/testimonials",{cache:"no-store"}),fetch("/api/cms/media",{cache:"no-store"})]).then(async([testimonialResponse,mediaResponse])=>{const testimonialJson=await testimonialResponse.json() as {testimonials?:TestimonialRecord[];message?:string},mediaJson=await mediaResponse.json() as {media?:MediaRecord[];message?:string};if(!testimonialResponse.ok)throw new Error(testimonialJson.message||"Testimoni gagal dimuat.");if(!mediaResponse.ok)throw new Error(mediaJson.message||"Media gagal dimuat.");setItems(testimonialJson.testimonials??[]);setMedia(mediaJson.media??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Testimoni gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(id:string,value:TestimonialRecord){setItems(current=>current.map(item=>item.id===id?value:item))}
  async function uploadPhoto(file:File,onSelected:(id:string)=>void){setUploading(true);setNotice({type:"info",message:`Mengunggah ${file.name}…`});try{const form=new FormData();form.append("file",file);form.append("altText",`Foto pelanggan ${file.name.replace(/\.[^.]+$/g,"")}`);const response=await fetch("/api/cms/media",{method:"POST",body:form}),json=await response.json() as {message?:string;media?:MediaRecord};if(!response.ok||!json.media)throw new Error(json.message||"Foto pelanggan gagal diunggah.");setMedia(current=>[json.media!,...current]);onSelected(json.media.id);setNotice({type:"success",message:`✓ ${json.message} Foto sudah dipilih untuk testimoni.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Foto pelanggan gagal diunggah."})}finally{setUploading(false)}}
  async function save(item:TestimonialRecord,isNew=false){setSavingId(isNew?"new":item.id);setNotice({type:"info",message:"Menyimpan testimoni…"});try{const response=await fetch("/api/cms/testimonials",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(item)}),json=await response.json() as {message?:string;testimonial?:TestimonialRecord};if(!response.ok||!json.testimonial)throw new Error(json.message||"Testimoni gagal disimpan.");if(isNew){setItems(current=>[...current,json.testimonial!].sort((a,b)=>a.sortOrder-b.sortOrder));setDraft(emptyTestimonial());setShowCreate(false);setOpenId(json.testimonial.id)}else setItems(current=>current.map(entry=>entry.id===json.testimonial?.id?json.testimonial!:entry).sort((a,b)=>a.sortOrder-b.sortOrder));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Testimoni gagal disimpan."})}finally{setSavingId(null)}}
  async function remove(item:TestimonialRecord){setSavingId(item.id);setNotice({type:"info",message:"Menghapus testimoni…"});try{const response=await fetch("/api/cms/testimonials",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Testimoni gagal dihapus.");setItems(current=>current.filter(entry=>entry.id!==item.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Testimoni gagal dihapus."})}finally{setSavingId(null)}}
  return <section className="cms-panel testimonial-manager"><div className="cms-panel-header"><div><h2>Kelola testimoni pelanggan</h2><p>Hanya testimoni asli dengan sumber tercatat dan status terverifikasi yang dapat ditampilkan di website.</p></div><div className="testimonial-toolbar"><a className="cms-button secondary" href="/#testimoni" target="_blank" rel="noreferrer">Lihat di beranda</a><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form testimoni ditutup.":"Form testimoni baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah testimoni"}</button></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{showCreate&&<form className="testimonial-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>Testimoni baru</strong><span>Masukkan isi asli, sumber, dan izin publikasinya.</span></div><span className="status-badge">Form terbuka</span></div><TestimonialFields item={draft} media={media} autoPhotoUpload uploading={uploading} onChange={setDraft} onOpenMedia={onOpenMedia} onUploadPhoto={file=>void uploadPhoto(file,id=>setDraft(current=>({...current,photoMediaId:id})))}/><div className="testimonial-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null||uploading} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null||uploading}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan testimoni"}</button></div></form>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat testimoni…</div>:items.length?<div className="testimonial-admin-list">{items.map(item=>{const isOpen=openId===item.id;return <article className={`testimonial-admin-item ${isOpen?"open":""}`} key={item.id}><button className="testimonial-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:item.id)}>{item.photoMediaId?<Image unoptimized src={`/api/cms/media/${item.photoMediaId}`} width={52} height={52} alt={item.photoAltText||item.name}/>:<span className="testimonial-admin-avatar">{testimonialInitials(item.name)}</span>}<span><strong>{item.name}</strong><small>{[item.serviceType,item.area].filter(Boolean).join(" · ")||"Belum ada keterangan pekerjaan"}</small></span><i className={`lead-status ${item.active&&item.verified?"status-published":"status-draft"}`}>{item.active&&item.verified?"Tampil":item.verified?"Terverifikasi":"Belum diverifikasi"}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="testimonial-editor" onSubmit={event=>{event.preventDefault();void save(item)}}><TestimonialFields item={item} media={media} autoPhotoUpload uploading={uploading} onChange={value=>update(item.id,value)} onOpenMedia={onOpenMedia} onUploadPhoto={file=>void uploadPhoto(file,id=>update(item.id,{...item,photoMediaId:id}))}/><div className="testimonial-editor-actions">{confirmDeleteId===item.id?<div className="faq-delete-confirm" role="alert"><span>Hapus testimoni ini?</span><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>void remove(item)}>{savingId===item.id?"Menghapus…":"Ya, hapus"}</button></div>:<button className="cms-button danger" type="button" disabled={savingId!==null||uploading} onClick={()=>setConfirmDeleteId(item.id)}>Hapus</button>}<button className="cms-button" type="submit" disabled={savingId!==null||uploading}>{savingId===item.id&&<span className="button-spinner"/>}{savingId===item.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Belum ada testimoni asli</h2><p>Klik Tambah testimoni setelah memperoleh izin pelanggan. Jangan memasukkan testimoni contoh atau hasil karangan.</p></div>}</section>
}

function emptyArea():AreaRecord{return{id:"",name:"",slug:"",city:"Kota Bogor",uniqueIntro:"",localDetails:"",geoLat:null,geoLng:null,seoTitle:"",metaDescription:"",robotsIndex:true,sortOrder:10,active:true,updatedAt:""}}
function areaSeoScore(area:AreaRecord){const checks=[area.seoTitle.length>=30&&area.seoTitle.length<=60,area.metaDescription.length>=120&&area.metaDescription.length<=160,area.uniqueIntro.length>=80,area.localDetails.length>=80],passed=checks.filter(Boolean).length;return{passed,total:checks.length,label:passed===4?"SEO baik":passed>=2?"Perlu dilengkapi":"SEO masih lemah",className:passed===4?"seo-good":passed>=2?"seo-warning":"seo-poor"}}
function AreaFields({area,autoSlug=false,onChange}:{area:AreaRecord;autoSlug?:boolean;onChange:(patch:Partial<AreaRecord>)=>void}){const seo=areaSeoScore(area);return <div className="area-editor-grid"><label>Nama area<input className="cms-input" required maxLength={160} value={area.name} onChange={event=>onChange({name:event.target.value,...(autoSlug?{slug:articleSlug(event.target.value),seoTitle:`Jasa Instalasi Listrik ${event.target.value} | Padjajaran Elektrik`,metaDescription:`Layanan instalasi, panel listrik, tambah titik, perbaikan, dan survei kelistrikan untuk ${event.target.value} dan sekitarnya.`}:{})})} placeholder="Contoh: Bogor Tengah"/></label><label>Slug halaman<input className="cms-input" required maxLength={180} value={area.slug} onChange={event=>onChange({slug:articleSlug(event.target.value)})} placeholder="bogor-tengah"/></label><label>Kota / kabupaten<select className="cms-input" value={area.city} onChange={event=>onChange({city:event.target.value})}><option>Kota Bogor</option><option>Kabupaten Bogor</option></select></label><label>Urutan tampil<input className="cms-input" type="number" min="0" max="9999" value={area.sortOrder} onChange={event=>onChange({sortOrder:Number(event.target.value)})}/></label><label className="span-2">Pendahuluan unik <small>{area.uniqueIntro.length} karakter · minimal 80</small><textarea className="cms-input" required rows={5} maxLength={3000} value={area.uniqueIntro} onChange={event=>onChange({uniqueIntro:event.target.value})} placeholder="Jelaskan jenis bangunan, karakter wilayah, atau kebutuhan listrik yang relevan untuk area ini."/></label><label className="span-2">Catatan survei lokal <small>{area.localDetails.length} karakter · minimal 80</small><textarea className="cms-input" required rows={5} maxLength={5000} value={area.localDetails} onChange={event=>onChange({localDetails:event.target.value})} placeholder="Tuliskan akses lokasi, kondisi umum, informasi yang perlu dikirim, atau pertimbangan survei yang benar-benar khusus untuk area ini."/></label><label>Latitude opsional<input className="cms-input" type="number" step="any" min="-90" max="90" value={area.geoLat??""} onChange={event=>onChange({geoLat:event.target.value===""?null:Number(event.target.value)})} placeholder="-6.5971"/></label><label>Longitude opsional<input className="cms-input" type="number" step="any" min="-180" max="180" value={area.geoLng??""} onChange={event=>onChange({geoLng:event.target.value===""?null:Number(event.target.value)})} placeholder="106.8060"/></label><label>SEO title <small>{area.seoTitle.length}/60</small><input className="cms-input" maxLength={220} value={area.seoTitle} onChange={event=>onChange({seoTitle:event.target.value})} placeholder={`Jasa Instalasi Listrik ${area.name}`}/></label><label>Meta description <small>{area.metaDescription.length}/160</small><textarea className="cms-input" rows={3} maxLength={320} value={area.metaDescription} onChange={event=>onChange({metaDescription:event.target.value})} placeholder="Ringkasan unik untuk hasil pencarian Google."/></label><div className={`area-seo-summary ${seo.className}`}><span className="seo-score-dot"/><div><strong>{seo.label} · {seo.passed}/{seo.total}</strong><small>Judul 30–60, meta 120–160, dan dua konten lokal minimal 80 karakter.</small></div></div><div className="area-visibility"><label className="check-label"><input type="checkbox" checked={area.robotsIndex} onChange={event=>onChange({robotsIndex:event.target.checked})}/><span>Izinkan halaman diindeks Google</span></label><label className="check-label"><input type="checkbox" checked={area.active} onChange={event=>onChange({active:event.target.checked})}/><span>Tampilkan area di website</span></label></div></div>}

function AreaManager(){
  const [items,setItems]=useState<AreaRecord[]>([]),[draft,setDraft]=useState<AreaRecord>(emptyArea()),[showCreate,setShowCreate]=useState(false),[openId,setOpenId]=useState<string|null>(null),[loading,setLoading]=useState(true),[savingId,setSavingId]=useState<string|null>(null),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{fetch("/api/cms/areas",{cache:"no-store"}).then(async response=>{const json=await response.json() as {areas?:AreaRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Area layanan gagal dimuat.");setItems(json.areas??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Area layanan gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(id:string,patch:Partial<AreaRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item))}
  async function save(area:AreaRecord,isNew=false){setSavingId(isNew?"new":area.id);setNotice({type:"info",message:"Menyimpan area layanan…"});try{const response=await fetch("/api/cms/areas",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(area)}),json=await response.json() as {message?:string;area?:AreaRecord};if(!response.ok||!json.area)throw new Error(json.message||"Area layanan gagal disimpan.");if(isNew){setItems(current=>[...current,json.area!].sort((a,b)=>a.sortOrder-b.sortOrder));setDraft(emptyArea());setShowCreate(false);setOpenId(json.area.id)}else setItems(current=>current.map(item=>item.id===json.area?.id?json.area!:item).sort((a,b)=>a.sortOrder-b.sortOrder));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Area layanan gagal disimpan."})}finally{setSavingId(null)}}
  async function importDefaults(){setSavingId("import");setNotice({type:"info",message:"Mengimpor 18 area awal…"});try{const response=await fetch("/api/cms/areas",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({importDefaults:true})}),json=await response.json() as {message?:string;areas?:AreaRecord[]};if(!response.ok||!json.areas)throw new Error(json.message||"Area awal gagal diimpor.");setItems(json.areas);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Area awal gagal diimpor."})}finally{setSavingId(null)}}
  async function remove(area:AreaRecord){setSavingId(area.id);setNotice({type:"info",message:"Menghapus area layanan…"});try{const response=await fetch("/api/cms/areas",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:area.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Area layanan gagal dihapus.");setItems(current=>current.filter(item=>item.id!==area.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Area layanan gagal dihapus."})}finally{setSavingId(null)}}
  return <section className="cms-panel area-manager"><div className="cms-panel-header"><div><h2>Kelola area layanan dan SEO lokal</h2><p>Setiap area harus memiliki informasi lokal yang unik. Area aktif otomatis tampil di beranda, daftar area, detail area, Page Builder, dan sitemap.</p></div><div className="area-toolbar"><a className="cms-button secondary" href="/area" target="_blank" rel="noreferrer">Lihat halaman area</a><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form area ditutup.":"Form area baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah area"}</button></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{showCreate&&<form className="area-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>Area layanan baru</strong><span>Lengkapi konten lokal dan SEO sebelum ditampilkan.</span></div><span className="status-badge">Form terbuka</span></div><AreaFields area={draft} autoSlug onChange={patch=>setDraft(current=>({...current,...patch}))}/><div className="area-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan area"}</button></div></form>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat area layanan…</div>:items.length?<div className="area-admin-list">{items.map(area=>{const isOpen=openId===area.id,seo=areaSeoScore(area);return <article className={`area-admin-item ${isOpen?"open":""}`} key={area.id}><button className="area-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:area.id)}><span className="area-order">{area.sortOrder}</span><span><strong>{area.name}</strong><small>{area.city} · SEO {seo.passed}/{seo.total}</small></span><i className={`lead-status ${area.active?"status-published":"status-draft"}`}>{area.active?"Tampil":"Disembunyikan"}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="area-editor" onSubmit={event=>{event.preventDefault();void save(area)}}><AreaFields area={area} onChange={patch=>update(area.id,patch)}/><div className="area-editor-actions">{confirmDeleteId===area.id?<div className="faq-delete-confirm" role="alert"><span>Hapus area ini?</span><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>void remove(area)}>{savingId===area.id?"Menghapus…":"Ya, hapus"}</button></div>:<button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(area.id)}>Hapus</button>} {area.active&&<a className="cms-button secondary" href={`/area/${area.slug}`} target="_blank" rel="noreferrer">Lihat halaman</a>}<button className="cms-button" type="submit" disabled={savingId!==null}>{savingId===area.id&&<span className="button-spinner"/>}{savingId===area.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Area bawaan belum masuk ke CMS</h2><p>Impor 18 area awal agar seluruh konten wilayah yang sekarang tampil dapat diedit, diurutkan, disembunyikan, dan dioptimalkan.</p><button className="cms-button" type="button" disabled={savingId!==null} onClick={()=>void importDefaults()}>{savingId==="import"&&<span className="button-spinner"/>}{savingId==="import"?"Mengimpor…":"Impor 18 area awal"}</button></div>}</section>
}

const faqCategories=[['general','Umum'],['price','Harga'],['survey','Survei'],['service','Layanan'],['safety','Keselamatan'],['warranty','Garansi'],['area','Area layanan']] as const;
function emptyFaq():FaqRecord{return{id:"",question:"",answer:"",category:"general",sortOrder:10,active:true,updatedAt:""}}
function FaqFields({faq,onChange}:{faq:FaqRecord;onChange:(patch:Partial<FaqRecord>)=>void}){return <div className="faq-editor-grid"><label className="span-2">Pertanyaan<input className="cms-input" required maxLength={300} value={faq.question} onChange={event=>onChange({question:event.target.value})} placeholder="Contoh: Apakah harga sudah termasuk material?"/></label><label className="span-2">Jawaban<textarea className="cms-input" required rows={5} maxLength={3000} value={faq.answer} onChange={event=>onChange({answer:event.target.value})} placeholder="Berikan jawaban yang jelas, faktual, dan tidak membuat klaim yang belum diverifikasi."/></label><label>Kategori<select className="cms-input" value={faq.category} onChange={event=>onChange({category:event.target.value})}>{faqCategories.map(([value,label])=><option value={value} key={value}>{label}</option>)}</select></label><label>Urutan tampil<input className="cms-input" type="number" min="0" max="9999" value={faq.sortOrder} onChange={event=>onChange({sortOrder:Number(event.target.value)})}/></label><label className="check-label span-2"><input type="checkbox" checked={faq.active} onChange={event=>onChange({active:event.target.checked})}/><span>Tampilkan FAQ ini di website</span></label></div>}
function FaqManager(){
  const [items,setItems]=useState<FaqRecord[]>([]),[draft,setDraft]=useState<FaqRecord>(emptyFaq()),[showCreate,setShowCreate]=useState(false),[openId,setOpenId]=useState<string|null>(null),[loading,setLoading]=useState(true),[savingId,setSavingId]=useState<string|null>(null),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{fetch("/api/cms/faqs",{cache:"no-store"}).then(async response=>{const json=await response.json() as {faqs?:FaqRecord[];message?:string};if(!response.ok)throw new Error(json.message||"FAQ gagal dimuat.");setItems(json.faqs??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"FAQ gagal dimuat."})).finally(()=>setLoading(false))},[]);
  function update(id:string,patch:Partial<FaqRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item))}
  async function save(faq:FaqRecord,isNew=false){setSavingId(isNew?"new":faq.id);setNotice({type:"info",message:"Menyimpan FAQ…"});try{const response=await fetch("/api/cms/faqs",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(faq)}),json=await response.json() as {message?:string;faq?:FaqRecord};if(!response.ok||!json.faq)throw new Error(json.message||"FAQ gagal disimpan.");if(isNew){setItems(current=>[...current,json.faq!].sort((a,b)=>a.sortOrder-b.sortOrder));setDraft(emptyFaq());setShowCreate(false);setOpenId(json.faq.id)}else setItems(current=>current.map(item=>item.id===json.faq?.id?json.faq!:item).sort((a,b)=>a.sortOrder-b.sortOrder));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"FAQ gagal disimpan."})}finally{setSavingId(null)}}
  async function importDefaults(){setSavingId("import");setNotice({type:"info",message:"Mengimpor FAQ awal…"});try{const response=await fetch("/api/cms/faqs",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({importDefaults:true})}),json=await response.json() as {message?:string;faqs?:FaqRecord[]};if(!response.ok||!json.faqs)throw new Error(json.message||"FAQ awal gagal diimpor.");setItems(json.faqs);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"FAQ awal gagal diimpor."})}finally{setSavingId(null)}}
  async function remove(faq:FaqRecord){setSavingId(faq.id);setNotice({type:"info",message:"Menghapus FAQ…"});try{const response=await fetch("/api/cms/faqs",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:faq.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"FAQ gagal dihapus.");setItems(current=>current.filter(item=>item.id!==faq.id));setConfirmDeleteId(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"FAQ gagal dihapus."})}finally{setSavingId(null)}}
  return <section className="cms-panel faq-manager"><div className="cms-panel-header"><div><h2>Kelola pertanyaan umum</h2><p>FAQ aktif otomatis tampil di beranda, halaman FAQ, dan block FAQ pada Page Builder.</p></div><div className="faq-toolbar"><a className="cms-button secondary" href="/faq" target="_blank" rel="noreferrer">Lihat halaman FAQ</a><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form FAQ ditutup.":"Form FAQ baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah FAQ"}</button></div></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{showCreate&&<form className="faq-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="article-editor-heading"><div><strong>FAQ baru</strong><span>Atur pertanyaan, jawaban, kategori, urutan, dan status tampil.</span></div><span className="status-badge">Form terbuka</span></div><FaqFields faq={draft} onChange={patch=>setDraft(current=>({...current,...patch}))}/><div className="article-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan FAQ"}</button></div></form>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat FAQ…</div>:items.length?<div className="faq-admin-list">{items.map(faq=>{const isOpen=openId===faq.id,categoryLabel=faqCategories.find(([value])=>value===faq.category)?.[1]||faq.category;return <article className={`faq-admin-item ${isOpen?"open":""}`} key={faq.id}><button className="faq-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:faq.id)}><span className="faq-order">{faq.sortOrder}</span><span><strong>{faq.question}</strong><small>{categoryLabel}</small></span><i className={`lead-status ${faq.active?"status-published":"status-draft"}`}>{faq.active?"Tampil":"Disembunyikan"}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="faq-editor" onSubmit={event=>{event.preventDefault();void save(faq)}}><FaqFields faq={faq} onChange={patch=>update(faq.id,patch)}/><div className="faq-editor-actions">{confirmDeleteId===faq.id?<div className="faq-delete-confirm" role="alert"><span>Hapus FAQ ini?</span><button className="cms-button secondary" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>void remove(faq)}>{savingId===faq.id?"Menghapus…":"Ya, hapus"}</button></div>:<button className="cms-button danger" type="button" disabled={savingId!==null} onClick={()=>setConfirmDeleteId(faq.id)}>Hapus</button>}<button className="cms-button" type="submit" disabled={savingId!==null}>{savingId===faq.id&&<span className="button-spinner"/>}{savingId===faq.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>:<div className="empty-state"><h2>Belum ada FAQ tersimpan di CMS</h2><p>Impor FAQ awal agar seluruh pertanyaan bawaan dapat diedit, diurutkan, disembunyikan, atau dihapus.</p><button className="cms-button" type="button" disabled={savingId!==null} onClick={()=>void importDefaults()}>{savingId==="import"&&<span className="button-spinner"/>}{savingId==="import"?"Mengimpor…":"Impor 10 FAQ awal"}</button></div>}</section>
}

function MediaManager({user}:{user:CmsUser}){
  const [items,setItems]=useState<MediaRecord[]>([]);
  const [loading,setLoading]=useState(true);
  const [uploading,setUploading]=useState(false);
  const [savingId,setSavingId]=useState<string|null>(null);
  const [deletingId,setDeletingId]=useState<string|null>(null);
  const [confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null);
  const [selectedId,setSelectedId]=useState<string|null>(null);
  const [query,setQuery]=useState("");
  const [selectedFile,setSelectedFile]=useState<{name:string;size:number;previewUrl:string}|null>(null);
  const [notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{
    fetch("/api/cms/media",{cache:"no-store"})
      .then(async response=>{const json=await response.json() as {media?:MediaRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Media gagal dimuat.");setItems(json.media??[])})
      .catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Media gagal dimuat."}))
      .finally(()=>setLoading(false));
  },[]);

  function chooseFile(file:File|null){
    setSelectedFile(current=>{if(current)URL.revokeObjectURL(current.previewUrl);return file?{name:file.name,size:file.size,previewUrl:URL.createObjectURL(file)}:null});
    if(file)setNotice({type:"info",message:`${file.name} siap diunggah.`});
  }

  async function upload(event:FormEvent<HTMLFormElement>){
    event.preventDefault();
    const form=event.currentTarget;
    setUploading(true);setNotice({type:"info",message:"Mengunggah dan memeriksa keamanan gambar…"});
    try{
      const response=await fetch("/api/cms/media",{method:"POST",body:new FormData(form)});
      const json=await response.json() as {message?:string;media?:MediaRecord};
      if(!response.ok||!json.media)throw new Error(json.message||"Gambar gagal diunggah.");
      setItems(current=>[json.media!,...current]);setSelectedId(json.media.id);form.reset();
      setSelectedFile(current=>{if(current)URL.revokeObjectURL(current.previewUrl);return null});
      setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Gambar gagal diunggah."})}finally{setUploading(false)}
  }

  function updateItem(id:string,patch:Partial<MediaRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item))}

  async function saveMetadata(item:MediaRecord){
    setSavingId(item.id);setNotice({type:"info",message:`Menyimpan informasi ${item.filename}…`});
    try{
      const response=await fetch("/api/cms/media",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id,altText:item.altText,caption:item.caption})});
      const json=await response.json() as {message?:string;media?:MediaRecord};
      if(!response.ok||!json.media)throw new Error(json.message||"Informasi gambar gagal disimpan.");
      setItems(current=>current.map(media=>media.id===json.media?.id?json.media!:media));setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Informasi gambar gagal disimpan."})}finally{setSavingId(null)}
  }

  async function deleteMedia(item:MediaRecord){
    setDeletingId(item.id);setNotice({type:"info",message:`Menghapus ${item.filename}…`});
    try{
      const response=await fetch("/api/cms/media",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id})});
      const json=await response.json() as {message?:string;id?:string};
      if(!response.ok)throw new Error(json.message||"Gambar gagal dihapus.");
      setItems(current=>current.filter(media=>media.id!==item.id));
      if(selectedId===item.id)setSelectedId(null);
      setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Gambar gagal dihapus."})}finally{setDeletingId(null);setConfirmDeleteId(null)}
  }

  async function copySelected(){
    const selected=items.find(item=>item.id===selectedId);if(!selected)return;
    try{await navigator.clipboard.writeText(selected.id);setNotice({type:"success",message:`✓ ID ${selected.filename} berhasil disalin.`})}catch{setNotice({type:"info",message:`ID gambar: ${selected.id}`})}
  }

  const filtered=items.filter(item=>`${item.filename} ${item.altText} ${item.caption}`.toLowerCase().includes(query.trim().toLowerCase()));
  const selected=items.find(item=>item.id===selectedId)??null;
  return <section className="cms-panel media-manager">
    <div className="cms-panel-header"><div><h2>Pustaka gambar</h2><p>Unggah gambar, lihat hasilnya, isi alt text, lalu pilih untuk digunakan pada konten.</p></div><span className="status-badge">{items.length} gambar</span></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    <form className="media-upload-panel" onSubmit={upload}>
      <label className={`media-upload-drop ${selectedFile?"has-file":""}`}><input required name="file" type="file" accept="image/jpeg,image/png,image/webp" onChange={event=>chooseFile(event.target.files?.[0]??null)}/>{selectedFile?<Image src={selectedFile.previewUrl} unoptimized width={110} height={82} alt="Pratinjau gambar yang akan diunggah"/>:<span className="media-upload-icon"><Icon name="upload"/></span>}<span><strong>{selectedFile?"Gambar siap diunggah":"Pilih gambar"}</strong><small>{selectedFile?`${selectedFile.name} · ${(selectedFile.size/1024/1024).toFixed(2)} MB`:"JPG, PNG, atau WebP · maksimal 5 MB"}</small></span></label>
      <div className="media-upload-fields"><label>Alt text<input className="cms-input" name="altText" maxLength={220} placeholder="Jelaskan isi gambar untuk aksesibilitas"/></label><label>Caption (opsional)<input className="cms-input" name="caption" maxLength={500} placeholder="Keterangan gambar"/></label></div>
      <button className="cms-button" type="submit" disabled={uploading||!selectedFile}>{uploading&&<span className="button-spinner"/>}{uploading?"Mengunggah…":"Unggah gambar"}</button>
    </form>
    {selected&&<div className="media-selected" role="status"><Image src={`/api/cms/media/${selected.id}`} unoptimized width={68} height={52} alt={selected.altText||selected.filename}/><span><strong>Gambar dipilih</strong><small>{selected.filename}</small></span><button className="cms-button secondary" type="button" onClick={()=>void copySelected()}>Salin ID gambar</button></div>}
    <div className="media-toolbar"><label>Cari gambar<input className="cms-input" type="search" value={query} onChange={event=>setQuery(event.target.value)} placeholder="Nama file, alt text, atau caption"/></label><span>{filtered.length} hasil</span></div>
    {loading?<div className="lead-loading"><span className="button-spinner"/> Memuat gambar…</div>:filtered.length?<div className="media-grid">{filtered.map(item=><article className={`media-card ${selectedId===item.id?"selected":""}`} key={item.id}><button className="media-preview-button" type="button" onClick={()=>setSelectedId(item.id)} aria-label={`Pilih ${item.filename}`}><Image src={`/api/cms/media/${item.id}`} unoptimized fill sizes="(max-width: 650px) 100vw, 260px" alt={item.altText||item.filename}/><span>{selectedId===item.id?"✓ Dipilih":"Pilih gambar"}</span></button><div className="media-card-body"><strong title={item.filename}>{item.filename}</strong><small>{(item.size/1024/1024).toFixed(2)} MB · {new Date(item.createdAt).toLocaleDateString("id-ID")}</small><label>Alt text<input className="cms-input" maxLength={220} value={item.altText} onChange={event=>updateItem(item.id,{altText:event.target.value})}/></label><label>Caption<input className="cms-input" maxLength={500} value={item.caption} onChange={event=>updateItem(item.id,{caption:event.target.value})}/></label><div className="media-card-actions"><button className="cms-button secondary" type="button" disabled={savingId!==null||deletingId!==null} onClick={()=>void saveMetadata(item)}>{savingId===item.id&&<span className="button-spinner"/>}{savingId===item.id?"Menyimpan…":"Simpan"}</button>{user.role==="super_admin"&&<button className="cms-button danger" type="button" disabled={savingId!==null||deletingId!==null} onClick={()=>setConfirmDeleteId(item.id)}>Hapus</button>}</div>{user.role==="super_admin"&&confirmDeleteId===item.id&&<div className="media-delete-confirm" role="alert"><strong>Hapus gambar ini?</strong><p>Gambar akan diarsipkan dari Media Library. Gambar yang masih digunakan pada konten tidak dapat dihapus.</p><div><button className="cms-button secondary" type="button" disabled={deletingId===item.id} onClick={()=>setConfirmDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={deletingId===item.id} onClick={()=>void deleteMedia(item)}>{deletingId===item.id&&<span className="button-spinner"/>}{deletingId===item.id?"Menghapus…":"Ya, hapus"}</button></div></div>}</div></article>)}</div>:<div className="empty-state"><h2>{query?"Gambar tidak ditemukan":"Belum ada gambar"}</h2><p>{query?"Coba kata pencarian lain.":"Unggah gambar pertama melalui formulir di atas."}</p></div>}
  </section>
}

const serviceIcons=[["home","Rumah"],["building","Gedung"],["panel","Panel"],["plus","Tambah titik"],["bolt","Listrik"],["ground","Grounding"],["cable","Kabel"],["check","Pemeriksaan"],["shield","Keamanan"]] as const;
const emptyService=():ServiceRecord=>({id:"",title:"",slug:"",excerpt:"",contentBody:"",icon:"bolt",status:"draft",sortOrder:0,seoTitle:"",metaDescription:"",updatedAt:""});
function serviceSlug(value:string){return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/(^-|-$)/g,"").slice(0,180)}

function ServiceFields({service,onChange,autoSlug=false}:{service:ServiceRecord;onChange:(patch:Partial<ServiceRecord>)=>void;autoSlug?:boolean}){
  return <div className="service-editor-grid">
    <label>Nama layanan<input className="cms-input" required maxLength={180} value={service.title} onChange={event=>onChange({title:event.target.value,...(autoSlug?{slug:serviceSlug(event.target.value)}:{})})} placeholder="Contoh: Instalasi Lampu Taman"/></label>
    <label>Slug halaman<input className="cms-input" required maxLength={180} value={service.slug} onChange={event=>onChange({slug:serviceSlug(event.target.value)})} placeholder="instalasi-lampu-taman"/></label>
    <label className="span-2">Ringkasan layanan<textarea className="cms-input" required rows={3} maxLength={500} value={service.excerpt} onChange={event=>onChange({excerpt:event.target.value})} placeholder="Ringkasan singkat yang tampil pada kartu layanan."/></label>
    <label className="span-2">Deskripsi tambahan<textarea className="cms-input" rows={5} maxLength={5000} value={service.contentBody} onChange={event=>onChange({contentBody:event.target.value})} placeholder="Jelaskan ruang lingkup atau manfaat layanan tanpa membuat klaim yang belum terverifikasi."/></label>
    <label>Ikon<select className="cms-input" value={service.icon} onChange={event=>onChange({icon:event.target.value})}>{serviceIcons.map(([value,label])=><option value={value} key={value}>{label}</option>)}</select></label>
    <label>Status<select className="cms-input" value={service.status} onChange={event=>onChange({status:event.target.value})}><option value="published">Aktif / diterbitkan</option><option value="draft">Draft / disembunyikan</option><option value="archived">Diarsipkan</option></select></label>
    <label>Urutan<input className="cms-input" type="number" min="0" max="9999" value={service.sortOrder} onChange={event=>onChange({sortOrder:Number(event.target.value)})}/></label>
    <label>SEO title<input className="cms-input" maxLength={220} value={service.seoTitle} onChange={event=>onChange({seoTitle:event.target.value})} placeholder="Otomatis jika dikosongkan"/></label>
    <label className="span-2">Meta description<textarea className="cms-input" rows={2} maxLength={320} value={service.metaDescription} onChange={event=>onChange({metaDescription:event.target.value})} placeholder="Otomatis menggunakan ringkasan jika dikosongkan"/></label>
  </div>
}

function ServiceManager(){
  const [items,setItems]=useState<ServiceRecord[]>([]);
  const [draft,setDraft]=useState<ServiceRecord>(emptyService());
  const [showCreate,setShowCreate]=useState(false);
  const [openId,setOpenId]=useState<string|null>(null);
  const [loading,setLoading]=useState(true);
  const [savingId,setSavingId]=useState<string|null>(null);
  const [notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{
    fetch("/api/cms/services",{cache:"no-store"})
      .then(async response=>{const json=await response.json() as {services?:ServiceRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Layanan gagal dimuat.");setItems(json.services??[])})
      .catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Layanan gagal dimuat."}))
      .finally(()=>setLoading(false));
  },[]);
  function updateItem(id:string,patch:Partial<ServiceRecord>){setItems(current=>current.map(item=>item.id===id?{...item,...patch}:item))}
  async function save(service:ServiceRecord,isNew=false){
    setSavingId(isNew?"new":service.id);setNotice({type:"info",message:"Menyimpan layanan…"});
    try{const response=await fetch("/api/cms/services",{method:isNew?"POST":"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(service)});const json=await response.json() as {message?:string;service?:ServiceRecord};if(!response.ok||!json.service)throw new Error(json.message||"Layanan gagal disimpan.");if(isNew){setItems(current=>[...current,json.service!].sort((a,b)=>a.sortOrder-b.sortOrder));setDraft(emptyService());setShowCreate(false);setOpenId(json.service.id)}else setItems(current=>current.map(item=>item.id===json.service?.id?json.service!:item).sort((a,b)=>a.sortOrder-b.sortOrder));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Layanan gagal disimpan."})}finally{setSavingId(null)}
  }

  return <section className="cms-panel service-manager">
    <div className="cms-panel-header"><div><h2>Kelola layanan</h2><p>Layanan berstatus aktif langsung tampil di beranda, halaman layanan, detail, dan sitemap.</p></div><button className={`cms-button ${showCreate?"secondary":""}`} type="button" onClick={()=>{setShowCreate(current=>!current);setNotice({type:"info",message:showCreate?"Form ditutup.":"Form layanan baru sudah dibuka."})}}>{showCreate?"Tutup form":"＋ Tambah layanan"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    {showCreate&&<form className="service-editor create" onSubmit={event=>{event.preventDefault();void save(draft,true)}}><div className="service-editor-heading"><div><strong>Layanan baru</strong><span>Isi data utama, pilih status, lalu simpan.</span></div><span className="status-badge">Form terbuka</span></div><ServiceFields service={draft} autoSlug onChange={patch=>setDraft(current=>({...current,...patch}))}/><div className="service-editor-actions"><button className="cms-button secondary" type="button" disabled={savingId==="new"} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={savingId!==null}>{savingId==="new"&&<span className="button-spinner"/>}{savingId==="new"?"Menyimpan…":"Simpan layanan"}</button></div></form>}
    {loading?<div className="lead-loading"><span className="button-spinner"/> Memuat layanan…</div>:<div className="service-admin-list">{items.map(service=>{const isOpen=openId===service.id;return <article className={`service-admin-item ${isOpen?"open":""}`} key={service.id}><button className="service-admin-summary" type="button" aria-expanded={isOpen} onClick={()=>setOpenId(isOpen?null:service.id)}><span className="service-admin-icon"><Icon name={service.icon}/></span><span><strong>{service.title}</strong><small>/layanan/{service.slug}</small></span><i className={`lead-status status-${service.status}`}>{service.status==="published"?"Aktif":service.status==="draft"?"Draft":"Arsip"}</i><b>{isOpen?"−":"+"}</b></button>{isOpen&&<form className="service-editor" onSubmit={event=>{event.preventDefault();void save(service)}}><ServiceFields service={service} onChange={patch=>updateItem(service.id,patch)}/><div className="service-editor-actions">{service.status==="published"&&<a className="cms-button secondary" href={`/layanan/${service.slug}`} target="_blank" rel="noreferrer">Lihat halaman</a>}<button className="cms-button" type="submit" disabled={savingId!==null}>{savingId===service.id&&<span className="button-spinner"/>}{savingId===service.id?"Menyimpan…":"Simpan perubahan"}</button></div></form>}</article>})}</div>}
  </section>
}

function PriceManager({prices}:{prices:Overview["prices"]}){
  const [rows,setRows]=useState(prices);
  const [showCreate,setShowCreate]=useState(false);
  const [newItem,setNewItem]=useState({name:"",price:"",unit:"titik",active:1});
  const [savingId,setSavingId]=useState<string|null>(null);
  const [creating,setCreating]=useState(false);
  const [notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{
    fetch("/api/cms/overview",{cache:"no-store"})
      .then(async response=>{
        const json=await response.json() as Overview&{message?:string};
        if(!response.ok)throw new Error(json.message||"Daftar harga gagal dimuat.");
        setRows(json.prices??[]);
      })
      .catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Daftar harga gagal dimuat."}));
  },[]);

  function updateRow(index:number,change:Partial<Overview["prices"][number]>) {
    setRows(current=>current.map((row,rowIndex)=>rowIndex===index?{...row,...change}:row));
  }

  async function save(row:Overview["prices"][number]){
    setSavingId(row.id);
    setNotice({type:"info",message:`Menyimpan ${row.name}…`});
    try{
      const response=await fetch("/api/cms/prices",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(row)});
      const json=await response.json() as {message:string;item?:Overview["prices"][number]};
      if(!response.ok)throw new Error(json.message||"Harga gagal disimpan.");
      if(json.item)setRows(current=>current.map(item=>item.id===json.item?.id?json.item:item));
      setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){
      setNotice({type:"error",message:error instanceof Error?error.message:"Harga gagal disimpan."});
    }finally{
      setSavingId(null);
    }
  }

  async function create(event:FormEvent<HTMLFormElement>){
    event.preventDefault();
    setCreating(true);
    setNotice({type:"info",message:"Menambahkan harga baru…"});
    try{
      const response=await fetch("/api/cms/prices",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({...newItem,price:newItem.price===""?null:Number(newItem.price)})});
      const json=await response.json() as {message:string;item?:Overview["prices"][number]};
      if(!response.ok||!json.item)throw new Error(json.message||"Harga gagal ditambahkan.");
      setRows(current=>[...current,json.item!]);
      setNewItem({name:"",price:"",unit:"titik",active:1});
      setShowCreate(false);
      setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){
      setNotice({type:"error",message:error instanceof Error?error.message:"Harga gagal ditambahkan."});
    }finally{
      setCreating(false);
    }
  }

  function openCreate(){
    setShowCreate(true);
    setNotice({type:"info",message:"Form tambah harga sudah dibuka. Lengkapi data lalu klik Simpan harga."});
  }

  return <section className="cms-panel price-manager">
    <div className="cms-panel-header">
      <div><h2>Daftar harga</h2><p>Tambah pekerjaan baru atau perbarui harga yang sudah tersimpan.</p></div>
      <button className={`cms-button ${showCreate?"secondary":""}`} type="button" aria-expanded={showCreate} onClick={()=>showCreate?setShowCreate(false):openCreate()}>{showCreate?"Tutup form":"＋ Tambah harga"}</button>
    </div>
    {showCreate&&<form className="price-create-form" onSubmit={create}>
      <div className="price-create-heading"><div><strong>Harga baru</strong><span>Item aktif akan langsung tersedia pada tabel publik dan kalkulator.</span></div><span className="status-badge">Form terbuka</span></div>
      <div className="price-create-grid">
        <label>Nama pekerjaan<input className="cms-input" required autoFocus maxLength={220} value={newItem.name} onChange={event=>setNewItem(current=>({...current,name:event.target.value}))} placeholder="Contoh: Pemasangan stop kontak"/></label>
        <label>Harga (Rp)<input className="cms-input" required type="number" min="0" max="1000000000" step="1000" value={newItem.price} onChange={event=>setNewItem(current=>({...current,price:event.target.value}))} placeholder="Contoh: 75000"/></label>
        <label>Satuan<input className="cms-input" required maxLength={40} value={newItem.unit} onChange={event=>setNewItem(current=>({...current,unit:event.target.value}))} placeholder="titik / unit / meter"/></label>
        <label>Status<select className="cms-input" value={newItem.active} onChange={event=>setNewItem(current=>({...current,active:Number(event.target.value)}))}><option value={1}>Aktif</option><option value={0}>Nonaktif</option></select></label>
      </div>
      <div className="price-create-actions"><button className="cms-button secondary" type="button" disabled={creating} onClick={()=>setShowCreate(false)}>Batal</button><button className="cms-button" type="submit" disabled={creating}>{creating&&<span className="button-spinner" aria-hidden="true"/>}{creating?"Menyimpan…":"Simpan harga"}</button></div>
    </form>}
    <div className="cms-table-wrap"><table className="cms-table"><thead><tr><th>Pekerjaan</th><th>Harga</th><th>Satuan</th><th>Status</th><th>Aksi</th></tr></thead><tbody>{rows.map((row,index)=><tr key={row.id}><td><input className="cms-input" value={row.name} maxLength={220} onChange={event=>updateRow(index,{name:event.target.value})}/></td><td><input className="cms-input" type="number" min="0" max="1000000000" value={row.price??""} onChange={event=>updateRow(index,{price:event.target.value===""?null:Number(event.target.value)})}/></td><td><input className="cms-input unit-input" value={row.unit} maxLength={40} onChange={event=>updateRow(index,{unit:event.target.value})}/></td><td><select className="cms-input status-select" value={row.active} onChange={event=>updateRow(index,{active:Number(event.target.value)})}><option value={1}>Aktif</option><option value={0}>Nonaktif</option></select></td><td><button className="cms-button save-price-button" type="button" disabled={savingId!==null} onClick={()=>save(row)}>{savingId===row.id&&<span className="button-spinner" aria-hidden="true"/>}{savingId===row.id?"Menyimpan…":"Simpan"}</button></td></tr>)}</tbody></table></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
  </section>
}

const leadStatuses=[
  ["new","Baru"],["contacted","Dihubungi"],["survey_scheduled","Dijadwalkan survei"],
  ["quotation_sent","Penawaran dikirim"],["negotiation","Negosiasi"],["won","Menang"],
  ["lost","Kalah"],["spam","Spam"],
] as const;

function leadStatusLabel(status:string){return leadStatuses.find(([value])=>value===status)?.[1]??status}
function urgencyLabel(urgency?:string){return urgency==="urgent"?"Mendesak":urgency==="soon"?"Dalam 1–3 hari":"Terencana"}
function leadDate(value?:string){if(!value)return"—";const parsed=new Date(value.includes("T")?value:`${value.replace(" ","T")}Z`);return Number.isNaN(parsed.getTime())?value:parsed.toLocaleString("id-ID",{dateStyle:"medium",timeStyle:"short",timeZone:"Asia/Jakarta"})}

function LeadManager({leads,user}:{leads:Overview["leads"],user:CmsUser}){
  const [items,setItems]=useState<LeadRecord[]>(leads);
  const [openId,setOpenId]=useState<string|null>(null);
  const [loading,setLoading]=useState(true);
  const [exporting,setExporting]=useState(false);
  const [updatingId,setUpdatingId]=useState<string|null>(null);
  const [notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);

  useEffect(()=>{
    fetch("/api/cms/leads",{cache:"no-store"})
      .then(async response=>{const json=await response.json() as {leads?:LeadRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Data lead gagal dimuat.");setItems(json.leads??[])})
      .catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Data lead gagal dimuat."}))
      .finally(()=>setLoading(false));
  },[]);

  async function exportCsv(){
    setExporting(true);
    setNotice({type:"info",message:"Menyiapkan file CSV…"});
    try{
      const response=await fetch("/api/cms/leads?format=csv",{cache:"no-store"});
      if(!response.ok){const json=await response.json() as {message?:string};throw new Error(json.message||"CSV gagal dibuat.")}
      const blob=await response.blob();
      const disposition=response.headers.get("content-disposition")??"";
      const filename=disposition.match(/filename="([^"]+)"/)?.[1]??"leads-padjajaran-elektrik.csv";
      const url=URL.createObjectURL(blob),anchor=document.createElement("a");
      anchor.href=url;anchor.download=filename;document.body.appendChild(anchor);anchor.click();anchor.remove();URL.revokeObjectURL(url);
      setNotice({type:"success",message:`✓ CSV berhasil diunduh (${items.length} lead).`});
    }catch(error){
      setNotice({type:"error",message:error instanceof Error?error.message:"CSV gagal dibuat."});
    }finally{setExporting(false)}
  }

  async function updateStatus(id:string,status:string){
    setUpdatingId(id);
    setNotice({type:"info",message:"Memperbarui status lead…"});
    try{
      const response=await fetch("/api/cms/leads",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({id,status})});
      const json=await response.json() as {message?:string};
      if(!response.ok)throw new Error(json.message||"Status lead gagal diperbarui.");
      setItems(current=>current.map(item=>item.id===id?{...item,status}:item));
      setNotice({type:"success",message:`✓ ${json.message}`});
    }catch(error){
      setNotice({type:"error",message:error instanceof Error?error.message:"Status lead gagal diperbarui."});
    }finally{setUpdatingId(null)}
  }

  return <section className="cms-panel lead-manager">
    <div className="cms-panel-header"><div><h2>Lead konsultasi</h2><p>{user.role==="sales_promotion"?"Data ditampilkan sesuai izin kampanye.":"Klik satu lead untuk melihat seluruh rincian, foto, dan sumbernya."}</p></div><button className="cms-button secondary" type="button" disabled={exporting||loading} onClick={exportCsv}>{exporting&&<span className="button-spinner" aria-hidden="true"/>}{exporting?"Menyiapkan…":"Ekspor CSV"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    {loading?<div className="lead-loading"><span className="button-spinner"/> Memuat rincian lead…</div>:items.length?<div className="lead-accordion-list">{items.map(lead=>{
      const isOpen=openId===lead.id;
      const whatsappDigits=(lead.whatsapp??"").replace(/\D/g,"").replace(/^0/,"62");
      return <article className={`lead-accordion ${isOpen?"open":""}`} key={lead.id}>
        <button className="lead-summary" type="button" onClick={()=>setOpenId(isOpen?null:lead.id)} aria-expanded={isOpen}>
          <span><small>Nama</small><strong>{lead.name}</strong></span><span><small>Layanan</small><strong>{lead.service}</strong></span><span><small>Status</small><i className={`lead-status status-${lead.status}`}>{leadStatusLabel(lead.status)}</i></span><span><small>Masuk</small><strong>{leadDate(lead.created_at)}</strong></span><b aria-hidden="true">{isOpen?"−":"+"}</b>
        </button>
        {isOpen&&<div className="lead-detail">
          <div className="lead-detail-grid">
            <div><small>WhatsApp</small>{lead.whatsapp?<a href={`https://wa.me/${whatsappDigits}`} target="_blank" rel="noreferrer">{lead.whatsapp}</a>:<strong>—</strong>}</div>
            <div><small>Email</small>{lead.email?<a href={`mailto:${lead.email}`}>{lead.email}</a>:<strong>—</strong>}</div>
            <div><small>Lokasi</small><strong>{lead.location||"—"}</strong></div>
            <div><small>Jenis bangunan</small><strong>{lead.building_type||"—"}</strong></div>
            <div><small>Jumlah titik</small><strong>{lead.quantity??"Belum ditentukan"}</strong></div>
            <div><small>Urgensi</small><strong>{urgencyLabel(lead.urgency)}</strong></div>
            <div><small>Jadwal survei</small><strong>{lead.requested_schedule?leadDate(lead.requested_schedule):"Belum ditentukan"}</strong></div>
            <div><small>ID lead</small><strong>{lead.id}</strong></div>
          </div>
          <div className="lead-description"><small>Deskripsi kebutuhan</small><p>{lead.description||"Tidak ada deskripsi."}</p></div>
          <div className="lead-detail-bottom">
            <div className="lead-photo-panel"><small>Foto kondisi</small>{lead.photo_media_id?<><Image src={`/api/cms/media/${lead.photo_media_id}`} unoptimized width={360} height={240} alt={`Foto kondisi dari ${lead.name}`}/><span>{lead.photo_filename||"Foto kondisi"}{lead.photo_size?` · ${(lead.photo_size/1024/1024).toFixed(2)} MB`:""}</span></>:<p>Tidak ada foto yang dikirim.</p>}</div>
            <div className="lead-source-panel"><small>Sumber lead</small><dl><div><dt>Landing page</dt><dd>{lead.landing_page||"—"}</dd></div><div><dt>UTM source</dt><dd>{lead.utm_source||"—"}</dd></div><div><dt>UTM medium</dt><dd>{lead.utm_medium||"—"}</dd></div><div><dt>UTM campaign</dt><dd>{lead.utm_campaign||"—"}</dd></div></dl></div>
          </div>
          <div className="lead-actions"><label>Status lead<select className="cms-input" value={lead.status} disabled={updatingId===lead.id||user.role!=="super_admin"} onChange={event=>updateStatus(lead.id,event.target.value)}>{leadStatuses.map(([value,label])=><option value={value} key={value}>{label}</option>)}</select></label>{lead.whatsapp&&<a className="cms-button" href={`https://wa.me/${whatsappDigits}`} target="_blank" rel="noreferrer"><Icon name="whatsapp"/> Hubungi WhatsApp</a>}</div>
        </div>}
      </article>})}</div>:<div className="empty-state"><h2>Belum ada lead</h2><p>Lead yang dikirim melalui formulir konsultasi akan tampil di sini.</p></div>}
  </section>
}

const menuLocationLabel:Record<MenuRecord["location"],string>={header:"Header utama",footer_services:"Footer — Layanan",footer_information:"Footer — Informasi",footer_legal:"Footer — Legal"};
function MenuManager(){
  const [menus,setMenus]=useState<MenuRecord[]>([]),[suggestions,setSuggestions]=useState<MenuSuggestion[]>([]),[activeLocation,setActiveLocation]=useState<MenuRecord["location"]>("header"),[selectedSuggestion,setSelectedSuggestion]=useState(""),[loading,setLoading]=useState(true),[saving,setSaving]=useState(false),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{fetch("/api/cms/menus",{cache:"no-store"}).then(async response=>{const json=await response.json() as {menus?:MenuRecord[];suggestions?:MenuSuggestion[];message?:string};if(!response.ok)throw new Error(json.message||"Menu gagal dimuat.");setMenus(json.menus??[]);setSuggestions(json.suggestions??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Menu gagal dimuat."})).finally(()=>setLoading(false))},[]);
  const menu=menus.find(item=>item.location===activeLocation),items=menu?.items??[],roots=items.filter(item=>!item.parentId),visibleRoots=roots.filter(item=>item.visible);
  function setItems(next:MenuItemRecord[]){setMenus(current=>current.map(item=>item.location===activeLocation?{...item,items:next.map((entry,index)=>({...entry,position:index}))}:item))}
  function update(id:string,patch:Partial<MenuItemRecord>){setItems(items.map(item=>item.id===id?{...item,...patch}:item))}
  function addBlank(){const next:MenuItemRecord={id:crypto.randomUUID(),parentId:null,label:"Menu baru",url:"/",position:items.length,target:"_self",visible:true};setItems([...items,next]);setNotice({type:"info",message:"Link baru ditambahkan. Lengkapi label dan URL, lalu simpan."})}
  function addSuggestion(){const suggestion=suggestions.find(item=>item.url===selectedSuggestion);if(!suggestion)return;setItems([...items,{id:crypto.randomUUID(),parentId:null,label:suggestion.label,url:suggestion.url,position:items.length,target:"_self",visible:true}]);setSelectedSuggestion("");setNotice({type:"info",message:`${suggestion.label} ditambahkan ke susunan menu.`})}
  function move(index:number,direction:-1|1){const target=index+direction;if(target<0||target>=items.length)return;const next=[...items],[item]=next.splice(index,1);next.splice(target,0,item);setItems(next)}
  function duplicate(item:MenuItemRecord,index:number){setItems([...items.slice(0,index+1),{...item,id:crypto.randomUUID(),label:`${item.label} (salinan)`,parentId:null},...items.slice(index+1)])}
  function remove(item:MenuItemRecord){setItems(items.filter(candidate=>candidate.id!==item.id&&candidate.parentId!==item.id));setConfirmDeleteId(null);setNotice({type:"info",message:`${item.label} dihapus dari susunan. Tekan Simpan agar perubahan diterapkan.`})}
  async function save(){if(!menu)return;setSaving(true);setNotice({type:"info",message:"Menyimpan menu…"});try{const response=await fetch("/api/cms/menus",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({location:menu.location,items})}),json=await response.json() as {message?:string;menu?:MenuRecord};if(!response.ok||!json.menu)throw new Error(json.message||"Menu gagal disimpan.");setMenus(current=>current.map(item=>item.location===json.menu?.location?json.menu!:item));setNotice({type:"success",message:`✓ ${json.message} Perubahan sudah diterapkan ke website.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Menu gagal disimpan."})}finally{setSaving(false)}}
  if(loading)return <section className="cms-panel lead-loading"><span className="button-spinner"/> Memuat navigasi…</section>;
  return <section className="cms-panel menu-manager">
    <div className="cms-panel-header"><div><h2>Menu & Navigasi</h2><p>Susun link header, kelompok footer, dropdown satu tingkat, target tab, dan visibilitas.</p></div><button className="cms-button" type="button" disabled={saving||!menu} onClick={()=>void save()}>{saving&&<span className="button-spinner"/>}{saving?"Menyimpan…":"Simpan & terapkan"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    <div className="menu-location-tabs">{menus.map(item=><button className={activeLocation===item.location?"active":""} type="button" key={item.location} onClick={()=>{setActiveLocation(item.location);setConfirmDeleteId(null);setNotice(null)}}><span>{menuLocationLabel[item.location]}</span><small>{item.items.filter(entry=>entry.visible).length} aktif</small></button>)}</div>
    {menu&&<div className="menu-manager-layout"><div className="menu-editor-panel"><div className="menu-add-panel"><div><strong>Tambahkan halaman</strong><span>Pilih halaman yang sudah tersedia atau buat link manual.</span></div><div><select className="cms-input" value={selectedSuggestion} onChange={event=>setSelectedSuggestion(event.target.value)}><option value="">Pilih halaman…</option>{suggestions.map(item=><option value={item.url} key={`${item.url}-${item.label}`}>{item.label} · {item.url}</option>)}</select><button className="cms-button secondary" type="button" disabled={!selectedSuggestion} onClick={addSuggestion}>＋ Tambahkan</button><button className="cms-button secondary" type="button" onClick={addBlank}>＋ Link manual</button></div></div><div className="menu-editor-heading"><span><strong>{menu.name}</strong><small>{menu.persisted?`Tersimpan ${pageDate(menu.updatedAt)}`:"Menggunakan susunan standar bawaan"}</small></span><b>{items.length} item</b></div>{items.length?<div className="menu-item-list">{items.map((item,index)=>{const hasChildren=items.some(candidate=>candidate.parentId===item.id);return <article className={`menu-item-editor ${item.parentId?"is-child":""} ${item.visible?"":"is-hidden"}`} key={item.id}><div className="menu-item-order"><span>⠿</span><button type="button" disabled={index===0} onClick={()=>move(index,-1)} title="Naik">↑</button><button type="button" disabled={index===items.length-1} onClick={()=>move(index,1)} title="Turun">↓</button></div><div className="menu-item-fields"><label>Label<input className="cms-input" maxLength={100} value={item.label} onChange={event=>update(item.id,{label:event.target.value})}/></label><label>URL<input className="cms-input" maxLength={500} value={item.url} onChange={event=>update(item.id,{url:event.target.value})} placeholder="/halaman atau https://…"/></label><label>Induk dropdown<select className="cms-input" disabled={activeLocation!=="header"||hasChildren} value={item.parentId??""} onChange={event=>update(item.id,{parentId:event.target.value||null})}><option value="">Tidak ada — menu utama</option>{roots.filter(parent=>parent.id!==item.id).map(parent=><option value={parent.id} key={parent.id}>{parent.label}</option>)}</select></label><label>Buka link<select className="cms-input" value={item.target} onChange={event=>update(item.id,{target:event.target.value as "_self"|"_blank"})}><option value="_self">Tab yang sama</option><option value="_blank">Tab baru</option></select></label></div><div className="menu-item-actions"><label className="check-label"><input type="checkbox" checked={item.visible} onChange={event=>update(item.id,{visible:event.target.checked})}/><span>{item.visible?"Ditampilkan":"Disembunyikan"}</span></label><button className="cms-button secondary" type="button" onClick={()=>duplicate(item,index)}>Salin</button>{confirmDeleteId===item.id?<><button className="cms-button danger" type="button" onClick={()=>remove(item)}>Ya, hapus</button><button className="cms-button secondary" type="button" onClick={()=>setConfirmDeleteId(null)}>Batal</button></>:<button className="cms-button danger" type="button" onClick={()=>setConfirmDeleteId(item.id)}>Hapus</button>}</div></article>})}</div>:<div className="empty-state compact"><h2>Menu ini kosong</h2><p>Tambahkan halaman atau link manual, lalu tekan Simpan & terapkan.</p></div>}</div><aside className="menu-preview-panel"><span className="eyebrow">Preview susunan</span><h3>{menu.name}</h3><p>Item tersembunyi tidak ditampilkan pada preview.</p>{activeLocation==="header"?<nav className="menu-header-preview">{visibleRoots.map(item=><div key={item.id}><span>{item.label}</span>{items.some(child=>child.parentId===item.id&&child.visible)&&<div>{items.filter(child=>child.parentId===item.id&&child.visible).map(child=><small key={child.id}>↳ {child.label}</small>)}</div>}</div>)}</nav>:<nav className="menu-footer-preview">{items.filter(item=>item.visible).map(item=><span key={item.id}>{item.label}<small>{item.url}</small></span>)}</nav>}<div className="menu-preview-note"><Icon name="check"/><span>Perubahan baru tampil setelah tombol <strong>Simpan & terapkan</strong> berhasil.</span></div></aside></div>}
  </section>
}

function emptyRedirect():RedirectRecord{return{id:"",fromPath:"",toPath:"",statusCode:301,hits:0,active:true,createdAt:"",updatedAt:""}}
function RedirectManager(){
  const [items,setItems]=useState<RedirectRecord[]>([]),[form,setForm]=useState<RedirectRecord|null>(null),[query,setQuery]=useState(""),[filter,setFilter]=useState<"all"|"active"|"inactive">("all"),[loading,setLoading]=useState(true),[saving,setSaving]=useState(false),[deletingId,setDeletingId]=useState<string|null>(null),[confirmDeleteId,setConfirmDeleteId]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{fetch("/api/cms/redirects",{cache:"no-store"}).then(async response=>{const json=await response.json() as {redirects?:RedirectRecord[];message?:string};if(!response.ok)throw new Error(json.message||"Redirect gagal dimuat.");setItems(json.redirects??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Redirect gagal dimuat."})).finally(()=>setLoading(false))},[]);
  const normalized=query.trim().toLowerCase(),visible=items.filter(item=>(filter==="all"||filter==="active"&&item.active||filter==="inactive"&&!item.active)&&(!normalized||`${item.fromPath} ${item.toPath}`.toLowerCase().includes(normalized))),totalHits=items.reduce((sum,item)=>sum+item.hits,0);
  function updateForm(patch:Partial<RedirectRecord>){setForm(current=>current?{...current,...patch}:current)}
  async function save(){if(!form)return;setSaving(true);setNotice({type:"info",message:form.id?"Menyimpan perubahan redirect…":"Menambahkan redirect…"});try{const response=await fetch("/api/cms/redirects",{method:form.id?"PATCH":"POST",headers:{"content-type":"application/json"},body:JSON.stringify(form)}),json=await response.json() as {message?:string;redirect?:RedirectRecord};if(!response.ok||!json.redirect)throw new Error(json.message||"Redirect gagal disimpan.");setItems(current=>form.id?current.map(item=>item.id===json.redirect?.id?json.redirect!:item):[json.redirect!,...current]);setForm(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Redirect gagal disimpan."})}finally{setSaving(false)}}
  async function toggle(item:RedirectRecord){setNotice({type:"info",message:item.active?"Menonaktifkan redirect…":"Mengaktifkan redirect…"});try{const response=await fetch("/api/cms/redirects",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({...item,active:!item.active})}),json=await response.json() as {message?:string;redirect?:RedirectRecord};if(!response.ok||!json.redirect)throw new Error(json.message||"Status redirect gagal diubah.");setItems(current=>current.map(entry=>entry.id===item.id?json.redirect!:entry));setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Status redirect gagal diubah."})}}
  async function remove(item:RedirectRecord){setDeletingId(item.id);setNotice({type:"info",message:"Menghapus redirect…"});try{const response=await fetch("/api/cms/redirects",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Redirect gagal dihapus.");setItems(current=>current.filter(entry=>entry.id!==item.id));setConfirmDeleteId(null);if(form?.id===item.id)setForm(null);setNotice({type:"success",message:`✓ ${json.message}`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Redirect gagal dihapus."})}finally{setDeletingId(null)}}
  return <section className="cms-panel redirect-manager">
    <div className="cms-panel-header"><div><h2>Redirect URL</h2><p>Arahkan URL lama ke halaman baru agar pengunjung dan mesin pencari tidak menemukan tautan rusak.</p></div><button className="cms-button" type="button" onClick={()=>{setForm(emptyRedirect());setConfirmDeleteId(null);setNotice(null)}}>＋ Tambah redirect</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    <div className="redirect-stats"><div><span>Total redirect</span><strong>{items.length}</strong></div><div><span>Redirect aktif</span><strong>{items.filter(item=>item.active).length}</strong></div><div><span>Total digunakan</span><strong>{totalHits}</strong></div></div>
    {form&&<div className="redirect-editor"><div className="redirect-editor-heading"><div><span className="eyebrow">{form.id?"Edit redirect":"Redirect baru"}</span><h3>{form.id?form.fromPath:"Hubungkan URL lama"}</h3></div><button className="cms-button secondary" type="button" disabled={saving} onClick={()=>setForm(null)}>Tutup</button></div><div className="redirect-form-grid"><label>URL lama<input className="cms-input" autoFocus required value={form.fromPath} onChange={event=>updateForm({fromPath:event.target.value})} placeholder="/halaman-lama"/><small>Gunakan path tanpa domain, query, atau tanda #.</small></label><span className="redirect-arrow" aria-hidden="true">→</span><label>Tujuan baru<input className="cms-input" required value={form.toPath} onChange={event=>updateForm({toPath:event.target.value})} placeholder="/halaman-baru"/><small>Hanya dapat menuju halaman internal website.</small></label><label>Jenis pengalihan<select className="cms-input" value={form.statusCode} onChange={event=>updateForm({statusCode:Number(event.target.value) as 301|302})}><option value="301">301 — Permanen, disarankan untuk perubahan slug</option><option value="302">302 — Sementara</option></select></label><label className="check-label"><input type="checkbox" checked={form.active} onChange={event=>updateForm({active:event.target.checked})}/><span>Aktifkan setelah disimpan</span></label></div><div className="redirect-editor-actions"><p><Icon name="shield"/> Sistem menolak redirect ke luar website, URL sistem, tujuan yang sama, dan redirect berputar.</p><button className="cms-button" type="button" disabled={saving||!form.fromPath.trim()||!form.toPath.trim()} onClick={()=>void save()}>{saving&&<span className="button-spinner"/>}{saving?"Menyimpan…":form.id?"Simpan perubahan":"Simpan & aktifkan"}</button></div></div>}
    <div className="redirect-toolbar"><label>Cari URL<input className="cms-input" type="search" value={query} onChange={event=>setQuery(event.target.value)} placeholder="URL lama atau tujuan"/></label><label>Status<select className="cms-input" value={filter} onChange={event=>setFilter(event.target.value as typeof filter)}><option value="all">Semua status</option><option value="active">Aktif</option><option value="inactive">Nonaktif</option></select></label><span>{visible.length} redirect</span></div>
    {loading?<div className="lead-loading"><span className="button-spinner"/> Memuat redirect…</div>:visible.length?<div className="redirect-list">{visible.map(item=><article className={`redirect-item ${item.active?"":"is-inactive"}`} key={item.id}><div className="redirect-paths"><a href={item.fromPath} target="_blank" rel="noreferrer">{item.fromPath}</a><span>→</span><a href={item.toPath} target="_blank" rel="noreferrer">{item.toPath}</a></div><div className="redirect-meta"><span className={`redirect-code code-${item.statusCode}`}>{item.statusCode}</span><span>{item.hits} kali digunakan</span><span>Diperbarui {pageDate(item.updatedAt)}</span></div><div className="redirect-actions"><button className="cms-button secondary" type="button" onClick={()=>{setForm({...item});setConfirmDeleteId(null);setNotice(null)}}>Edit</button><button className="cms-button secondary" type="button" onClick={()=>void toggle(item)}>{item.active?"Nonaktifkan":"Aktifkan"}</button>{confirmDeleteId===item.id?<><button className="cms-button danger" type="button" disabled={deletingId!==null} onClick={()=>void remove(item)}>{deletingId===item.id?"Menghapus…":"Ya, hapus"}</button><button className="cms-button secondary" type="button" onClick={()=>setConfirmDeleteId(null)}>Batal</button></>:<button className="cms-button danger" type="button" disabled={deletingId!==null} onClick={()=>setConfirmDeleteId(item.id)}>Hapus</button>}</div></article>)}</div>:<div className="empty-state"><h2>{query||filter!=="all"?"Redirect tidak ditemukan":"Belum ada redirect"}</h2><p>{query||filter!=="all"?"Coba ubah pencarian atau filter status.":"Perubahan slug halaman dan artikel akan membuat redirect otomatis. Anda juga dapat menambahkannya secara manual."}</p>{!query&&filter==="all"&&<button className="cms-button" type="button" onClick={()=>setForm(emptyRedirect())}>Tambah redirect pertama</button>}</div>}
  </section>
}

function IdentityMediaField({title,hint,mediaId,media,uploading,onSelect,onUpload}:{title:string;hint:string;mediaId:string;media:MediaRecord[];uploading:boolean;onSelect:(id:string)=>void;onUpload:(file:File)=>void}){
  const selected=media.find(item=>item.id===mediaId);
  return <article className="identity-media-card"><div className="identity-media-heading"><div><strong>{title}</strong><span>{hint}</span></div>{mediaId&&<span className="identity-ready">✓ Dipilih</span>}</div><div className="identity-media-preview">{selected?<Image src={`/api/cms/media/${selected.id}`} fill unoptimized alt={selected.altText||selected.filename}/>:<span><Icon name="image"/><small>Belum dipilih</small></span>}</div><select className="cms-input" value={mediaId} onChange={event=>onSelect(event.target.value)}><option value="">Gunakan identitas bawaan</option>{media.map(item=><option value={item.id} key={item.id}>{item.filename}</option>)}</select><div className="identity-media-actions"><label className={`cms-button secondary ${uploading?"disabled":""}`}>{uploading?<><span className="button-spinner"/> Mengunggah…</>:<>＋ Unggah baru</>}<input type="file" accept="image/jpeg,image/png,image/webp" disabled={uploading} onChange={event=>{const file=event.currentTarget.files?.[0];if(file)onUpload(file);event.currentTarget.value=""}}/></label>{mediaId&&<button className="cms-button secondary" type="button" disabled={uploading} onClick={()=>onSelect("")}>Hapus pilihan</button>}</div></article>
}

function BusinessSettingsManager(){
  const [form,setForm]=useState<BusinessSettings>(defaultBusinessSettings),[media,setMedia]=useState<MediaRecord[]>([]),[loading,setLoading]=useState(true),[saving,setSaving]=useState(false),[uploadingTarget,setUploadingTarget]=useState<"logo_media_id"|"favicon_media_id"|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{Promise.all([fetch("/api/cms/business-settings",{cache:"no-store"}),fetch("/api/cms/media",{cache:"no-store"})]).then(async([settingsResponse,mediaResponse])=>{const settingsJson=await settingsResponse.json() as {settings?:Partial<BusinessSettings>;message?:string},mediaJson=await mediaResponse.json() as {media?:MediaRecord[];message?:string};if(!settingsResponse.ok)throw new Error(settingsJson.message||"Identitas bisnis gagal dimuat.");if(!mediaResponse.ok)throw new Error(mediaJson.message||"Media Library gagal dimuat.");if(settingsJson.settings)setForm(current=>({...current,...settingsJson.settings}));setMedia(mediaJson.media??[])}).catch(error=>setNotice({type:"error",message:error instanceof Error?error.message:"Identitas bisnis belum dapat dimuat."})).finally(()=>setLoading(false))},[]);
  function update<K extends keyof BusinessSettings>(key:K,value:BusinessSettings[K]){setForm(current=>({...current,[key]:value}))}
  async function uploadIdentity(file:File,target:"logo_media_id"|"favicon_media_id"){
    setUploadingTarget(target);setNotice({type:"info",message:`Mengunggah ${target==="logo_media_id"?"logo":"favicon"} dan memeriksa keamanan gambar…`});
    try{const payload=new FormData();payload.append("file",file);payload.append("altText",target==="logo_media_id"?`Logo ${form.business_name}`:`Ikon ${form.business_name}`);payload.append("caption",target==="logo_media_id"?"Logo utama website":"Favicon website");const response=await fetch("/api/cms/media",{method:"POST",body:payload}),json=await response.json() as {message?:string;media?:MediaRecord};if(!response.ok||!json.media)throw new Error(json.message||"Gambar gagal diunggah.");setMedia(current=>[json.media!,...current.filter(item=>item.id!==json.media?.id)]);setForm(current=>({...current,[target]:json.media!.id}));setNotice({type:"success",message:`✓ ${target==="logo_media_id"?"Logo":"Favicon"} berhasil diunggah dan dipilih. Tekan Simpan & terapkan agar tampil di website.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Gambar gagal diunggah."})}finally{setUploadingTarget(null)}
  }
  async function save(){setSaving(true);setNotice({type:"info",message:"Menyimpan identitas dan kontak…"});try{const response=await fetch("/api/cms/business-settings",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(form)}),json=await response.json() as {message?:string;settings?:BusinessSettings};if(!response.ok||!json.settings)throw new Error(json.message||"Identitas bisnis gagal disimpan.");setForm(json.settings);setNotice({type:"success",message:`✓ ${json.message} Logo, favicon, header, footer, dan metadata sudah diperbarui.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Identitas bisnis gagal disimpan."})}finally{setSaving(false)}}
  const logo=media.find(item=>item.id===form.logo_media_id),favicon=media.find(item=>item.id===form.favicon_media_id);
  return <section className="cms-panel identity-manager"><div className="cms-panel-header"><div><h2>Identitas & Kontak</h2><p>Kelola logo, favicon, nama bisnis, informasi legal, kontak, dan jam operasional dari satu tempat.</p></div><button className="cms-button" type="button" disabled={loading||saving||uploadingTarget!==null} onClick={()=>void save()}>{saving&&<span className="button-spinner"/>}{saving?"Menyimpan…":"Simpan & terapkan"}</button></div>{notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}{loading?<div className="lead-loading"><span className="button-spinner"/> Memuat identitas website…</div>:<><div className="identity-live-preview"><div><span className="eyebrow">Preview identitas</span><div className="identity-preview-brand">{logo?<Image src={`/api/cms/media/${logo.id}`} width={190} height={56} unoptimized alt={logo.altText||logo.filename}/>:<><span className="brand-mark"><Icon name="bolt"/></span><strong>{form.business_name}</strong></>}</div><small>{form.tagline||"Tagline belum diisi"}</small></div><div className="identity-preview-favicon">{favicon?<Image src={`/api/cms/media/${favicon.id}`} width={52} height={52} unoptimized alt={favicon.altText||favicon.filename}/>:<span><Icon name="bolt"/></span>}<small>Favicon browser</small></div></div><section className="identity-section"><div className="identity-section-heading"><h3>Logo & ikon website</h3><p>Gunakan PNG transparan atau WebP. Logo disarankan horizontal; favicon disarankan persegi.</p></div><div className="identity-media-grid"><IdentityMediaField title="Logo utama" hint="Tampil pada header, footer, dan halaman login CMS." mediaId={form.logo_media_id} media={media} uploading={uploadingTarget==="logo_media_id"} onSelect={id=>update("logo_media_id",id)} onUpload={file=>void uploadIdentity(file,"logo_media_id")}/><IdentityMediaField title="Favicon" hint="Ikon kecil pada tab browser dan bookmark." mediaId={form.favicon_media_id} media={media} uploading={uploadingTarget==="favicon_media_id"} onSelect={id=>update("favicon_media_id",id)} onUpload={file=>void uploadIdentity(file,"favicon_media_id")}/></div></section><section className="identity-section"><div className="identity-section-heading"><h3>Profil bisnis</h3><p>Informasi kosong tidak akan ditampilkan pada frontend.</p></div><div className="form-grid"><label>Nama bisnis<input className="cms-input" required maxLength={100} value={form.business_name} onChange={event=>update("business_name",event.target.value)}/></label><label>Tagline<input className="cms-input" maxLength={200} value={form.tagline} onChange={event=>update("tagline",event.target.value)}/></label><label>Nama legal perusahaan<input className="cms-input" maxLength={160} value={form.legal_name} onChange={event=>update("legal_name",event.target.value)} placeholder="Kosongkan sampai terverifikasi"/></label><label>Nomor legalitas / NIB<input className="cms-input" maxLength={100} value={form.registration_number} onChange={event=>update("registration_number",event.target.value)} placeholder="Kosongkan sampai terverifikasi"/></label><label className="span-2">Deskripsi singkat<textarea className="cms-input" rows={3} maxLength={500} value={form.short_description} onChange={event=>update("short_description",event.target.value)} placeholder="Deskripsi singkat untuk footer, metadata, dan profil bisnis"/><small>{form.short_description.length}/500 karakter</small></label><label>Area layanan<input className="cms-input" maxLength={160} value={form.service_area} onChange={event=>update("service_area",event.target.value)}/></label><label>Garansi pekerjaan<input className="cms-input" maxLength={80} value={form.warranty} onChange={event=>update("warranty",event.target.value)}/></label></div></section><section className="identity-section"><div className="identity-section-heading"><h3>Kontak & lokasi</h3><p>Nomor dan tautan akan langsung digunakan oleh tombol kontak publik.</p></div><div className="form-grid"><label>Nomor WhatsApp<input className="cms-input" inputMode="tel" value={form.whatsapp_number} onChange={event=>update("whatsapp_number",event.target.value)} placeholder="Contoh: 0812xxxx atau +62812xxxx"/></label><label>Nomor telepon<input className="cms-input" inputMode="tel" value={form.phone_number} onChange={event=>update("phone_number",event.target.value)} placeholder="Kosongkan jika sama dengan WhatsApp"/></label><label>Email publik<input className="cms-input" type="email" value={form.public_email} onChange={event=>update("public_email",event.target.value)}/></label><label>Google Maps URL<input className="cms-input" type="url" value={form.maps_url} onChange={event=>update("maps_url",event.target.value)} placeholder="https://maps.app.goo.gl/..."/></label><label className="span-2">Alamat<textarea className="cms-input" rows={3} value={form.address} onChange={event=>update("address",event.target.value)} placeholder="Kosongkan sampai alamat siap dipublikasikan"/></label><label>Jam Senin–Jumat<input className="cms-input" value={form.hours_weekdays} onChange={event=>update("hours_weekdays",event.target.value)}/></label><label>Jam Sabtu<input className="cms-input" value={form.hours_saturday} onChange={event=>update("hours_saturday",event.target.value)}/></label></div></section><section className="identity-section"><div className="identity-section-heading"><h3>Media sosial</h3><p>Hanya URL HTTPS yang valid yang akan disimpan.</p></div><div className="form-grid"><label>Instagram URL<input className="cms-input" type="url" value={form.instagram_url} onChange={event=>update("instagram_url",event.target.value)} placeholder="https://instagram.com/..."/></label><label>Facebook URL<input className="cms-input" type="url" value={form.facebook_url} onChange={event=>update("facebook_url",event.target.value)} placeholder="https://facebook.com/..."/></label><label>TikTok URL<input className="cms-input" type="url" value={form.tiktok_url} onChange={event=>update("tiktok_url",event.target.value)} placeholder="https://tiktok.com/@..."/></label></div></section></>}</section>
}

function IntegrationManager(){
  const [form,setForm]=useState({ga4_measurement_id:"",ga4_property_id:"",gtm_container_id:"",search_console_meta:"",search_console_site_url:"",bing_verification:"",maps_embed:"",analytics_admin_exclusion:true}),[status,setStatus]=useState(""),[saving,setSaving]=useState(false),[runtime,setRuntime]=useState<{configured:boolean;email:string}>({configured:false,email:""}),[testing,setTesting]=useState(false);
  useEffect(()=>{fetch("/api/cms/settings",{cache:"no-store"}).then(r=>r.json()).then((x:{settings?:Partial<typeof form>;runtime?:{googleServiceAccount?:{configured:boolean;email:string}}})=>{if(x.settings)setForm(current=>({...current,...x.settings}));if(x.runtime?.googleServiceAccount)setRuntime(x.runtime.googleServiceAccount)}).catch(()=>setStatus("Pengaturan belum dapat dimuat."))},[]);
  async function save(){setSaving(true);setStatus("Menyimpan pengaturan…");try{const res=await fetch("/api/cms/settings",{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify(form)}),json=await res.json() as {message:string};setStatus(json.message)}finally{setSaving(false)}}
  async function testConnection(){setTesting(true);setStatus("Menguji koneksi GA4 dan Search Console…");try{const res=await fetch("/api/cms/analytics?days=7",{cache:"no-store"}),json=await res.json() as {configured?:{ga4:boolean;searchConsole:boolean};errors?:{ga4:string|null;searchConsole:string|null};message?:string};if(!res.ok)throw new Error(json.message||"Pengujian gagal.");const errors=[json.errors?.ga4,json.errors?.searchConsole].filter(Boolean);setStatus(errors.length?errors.join(" "):`Koneksi diuji. GA4: ${json.configured?.ga4?"siap":"belum lengkap"}; Search Console: ${json.configured?.searchConsole?"siap":"belum lengkap"}.`)}catch(error){setStatus(error instanceof Error?error.message:"Pengujian gagal.")}finally{setTesting(false)}}
  return <section className="cms-panel integration-manager"><div className="cms-panel-header"><div><h2>Integrasi Google & SEO teknis</h2><p>ID pelacakan mengumpulkan data publik; Property ID dan Search Console digunakan untuk grafik dashboard.</p></div><div className="integration-actions"><button className="cms-button secondary" type="button" disabled={testing} onClick={()=>void testConnection()}>{testing?<><span className="button-spinner"/>Menguji…</>:"Uji koneksi"}</button><button className="cms-button" type="button" disabled={saving} onClick={()=>void save()}>{saving?<><span className="button-spinner"/>Menyimpan…</>:"Simpan pengaturan"}</button></div></div><div className={`google-runtime-status ${runtime.configured?"connected":""}`}><span>{runtime.configured?"Service Account server siap":"Service Account server belum dipasang"}</span>{runtime.email&&<strong>{runtime.email}</strong>}<small>Kredensial rahasia tidak pernah ditampilkan atau disimpan melalui formulir CMS.</small></div><div className="form-grid"><label>GA4 Measurement ID<input className="cms-input" value={form.ga4_measurement_id} onChange={e=>setForm({...form,ga4_measurement_id:e.target.value})} placeholder="G-XXXXXXXXXX"/><small>Digunakan untuk mencatat kunjungan dari website.</small></label><label>GA4 Property ID<input className="cms-input" value={form.ga4_property_id} onChange={e=>setForm({...form,ga4_property_id:e.target.value})} placeholder="123456789" inputMode="numeric"/><small>Angka Property ID, bukan Measurement ID.</small></label><label>GTM Container ID<input className="cms-input" value={form.gtm_container_id} onChange={e=>setForm({...form,gtm_container_id:e.target.value})} placeholder="GTM-XXXXXXX"/></label><label>Properti Search Console<input className="cms-input" value={form.search_console_site_url} onChange={e=>setForm({...form,search_console_site_url:e.target.value})} placeholder="sc-domain:domainanda.com"/><small>Salin persis dari Search Console.</small></label><label>Search Console meta verification<input className="cms-input" value={form.search_console_meta} onChange={e=>setForm({...form,search_console_meta:e.target.value})}/></label><label>Bing verification<input className="cms-input" value={form.bing_verification} onChange={e=>setForm({...form,bing_verification:e.target.value})}/></label><label className="span-2">Google Maps embed URL<input className="cms-input" value={form.maps_embed} onChange={e=>setForm({...form,maps_embed:e.target.value})}/></label><label className="check-label span-2"><input type="checkbox" checked={form.analytics_admin_exclusion} onChange={e=>setForm({...form,analytics_admin_exclusion:e.target.checked})}/><span>Jangan hitung traffic administrator</span></label></div>{status&&<p className="form-message info" role="status">{status}</p>}</section>
}

const auditActionNames:Record<string,string>={"login.success":"Login berhasil","login.failed":"Login gagal","logout.success":"Logout berhasil","setup.super_admin":"Setup Super Admin","password.owner_reset":"Password pemilik diubah","password.owner_reset_failed":"Reset password gagal","security.session_revoked":"Sesi pengguna diputus","security.other_sessions_revoked":"Semua sesi lain diputus","security.login_locks_released":"Pembatasan login dilepas","backup.created":"Backup dibuat","backup.verified":"Backup diverifikasi","backup.restored":"Database dipulihkan","backup.deleted":"Backup dihapus","user.created":"User dibuat","page.created":"Halaman dibuat","page.updated":"Halaman diperbarui","page.archived":"Halaman diarsipkan","page.version_restored":"Versi halaman dipulihkan","article.created":"Artikel dibuat","article.updated":"Artikel diperbarui","service.created":"Layanan dibuat","service.updated":"Layanan diperbarui","price.created":"Harga dibuat","price.updated":"Harga diperbarui","lead.status_updated":"Status lead diperbarui","settings.business.updated":"Kontak bisnis diperbarui","settings.google.updated":"Integrasi Google diperbarui","menu.updated":"Menu diperbarui","redirect.created":"Redirect dibuat","redirect.updated":"Redirect diperbarui","redirect.archived":"Redirect dihapus","media.uploaded":"Gambar diunggah","media.updated":"Informasi gambar diperbarui","media.archived":"Gambar dihapus"};
const auditEntityNames:Record<string,string>={auth:"Autentikasi",user:"User",page:"Halaman",post:"Artikel",service:"Layanan",price_item:"Harga",lead:"Lead",site_settings:"Pengaturan situs",menu:"Menu",redirect:"Redirect",media:"Media",faq:"FAQ",service_area:"Area layanan",project:"Proyek",testimonial:"Testimoni",promotion:"Promo",category:"Kategori",tag:"Tag",backup:"Backup"};
function auditActionLabel(action:string){if(auditActionNames[action])return auditActionNames[action];const [subject,verb]=action.split(".");const verbs:Record<string,string>={created:"dibuat",updated:"diperbarui",archived:"dihapus",uploaded:"diunggah",defaults_imported:"data awal diimpor"};return`${auditEntityNames[subject]||subject.replaceAll("_"," ")} ${verbs[verb]||verb?.replaceAll("_"," ")||"diubah"}`}
function auditTone(action:string){return action.includes("failed")?"danger":action.includes("archived")||action.includes("password")||action.startsWith("user.")?"warning":"success"}
function auditDeviceLabel(agent:string){if(!agent)return"Perangkat tidak tercatat";const browser=/Edg\//.test(agent)?"Edge":/Chrome\//.test(agent)?"Chrome":/Firefox\//.test(agent)?"Firefox":/Safari\//.test(agent)?"Safari":"Browser";const os=/Windows/.test(agent)?"Windows":/Android/.test(agent)?"Android":/iPhone|iPad/.test(agent)?"iPhone/iPad":/Mac OS/.test(agent)?"macOS":/Linux/.test(agent)?"Linux":"perangkat lain";return`${browser} · ${os}`}
function auditDate(value:string){if(!value)return"—";const normalized=/Z$|[+-]\d\d:?\d\d$/.test(value)?value:`${value.replace(" ","T")}Z`,date=new Date(normalized);return Number.isNaN(date.getTime())?value:date.toLocaleString("id-ID",{dateStyle:"medium",timeStyle:"short"})}
function auditSnapshot(value:unknown){if(value===null||value===undefined)return"Tidak ada data";if(typeof value==="string")return value;try{return JSON.stringify(value,null,2)}catch{return"Data tidak dapat ditampilkan"}}

function AuditLogManager(){
  const [data,setData]=useState<AuditResponse|null>(null),[loading,setLoading]=useState(true),[exporting,setExporting]=useState(false),[openId,setOpenId]=useState<string|null>(null),[page,setPage]=useState(1),[query,setQuery]=useState(""),[appliedQuery,setAppliedQuery]=useState(""),[category,setCategory]=useState("all"),[entityType,setEntityType]=useState(""),[userId,setUserId]=useState(""),[dateFrom,setDateFrom]=useState(""),[dateTo,setDateTo]=useState(""),[reloadKey,setReloadKey]=useState(0),[notice,setNotice]=useState<{type:"error"|"info";message:string}|null>(null);
  useEffect(()=>{let cancelled=false;const params=new URLSearchParams({page:String(page),perPage:"25"});if(appliedQuery)params.set("q",appliedQuery);if(category!=="all")params.set("category",category);if(entityType)params.set("entityType",entityType);if(userId)params.set("userId",userId);if(dateFrom)params.set("from",dateFrom);if(dateTo)params.set("to",dateTo);fetch(`/api/cms/audit-logs?${params}`,{cache:"no-store"}).then(async response=>{const json=await response.json() as AuditResponse;if(!response.ok)throw new Error(json.message||"Audit Log gagal dimuat.");if(!cancelled){setData(json);setOpenId(null)}}).catch(error=>{if(!cancelled)setNotice({type:"error",message:error instanceof Error?error.message:"Audit Log gagal dimuat."})}).finally(()=>{if(!cancelled)setLoading(false)});return()=>{cancelled=true}},[page,appliedQuery,category,entityType,userId,dateFrom,dateTo,reloadKey]);
  function beginReload(){setLoading(true);setNotice(null);setReloadKey(current=>current+1)}
  function applySearch(event:FormEvent<HTMLFormElement>){event.preventDefault();beginReload();setPage(1);setAppliedQuery(query.trim())}
  function resetFilters(){beginReload();setQuery("");setAppliedQuery("");setCategory("all");setEntityType("");setUserId("");setDateFrom("");setDateTo("");setPage(1)}
  async function exportCsv(){setExporting(true);setNotice({type:"info",message:"Menyiapkan ekspor Audit Log…"});try{const params=new URLSearchParams({export:"csv"});if(appliedQuery)params.set("q",appliedQuery);if(category!=="all")params.set("category",category);if(entityType)params.set("entityType",entityType);if(userId)params.set("userId",userId);if(dateFrom)params.set("from",dateFrom);if(dateTo)params.set("to",dateTo);const response=await fetch(`/api/cms/audit-logs?${params}`,{cache:"no-store"});if(!response.ok){const json=await response.json() as {message?:string};throw new Error(json.message||"Ekspor gagal.")}const blob=await response.blob(),href=URL.createObjectURL(blob),link=document.createElement("a");link.href=href;link.download=`audit-log-${new Date().toISOString().slice(0,10)}.csv`;document.body.appendChild(link);link.click();link.remove();URL.revokeObjectURL(href);setNotice({type:"info",message:"✓ File Audit Log berhasil diunduh."})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Ekspor Audit Log gagal."})}finally{setExporting(false)}}
  const summary=data?.summary??{total:0,lastDay:0,loginFailed:0,contentChanges:0},pagination=data?.pagination??{page:1,perPage:25,total:0,totalPages:1};
  return <section className="cms-panel audit-manager">
    <div className="cms-panel-header"><div><h2>Audit Log</h2><p>Riwayat keamanan dan perubahan CMS. Catatan ini hanya dapat dilihat Super Admin dan tidak dapat dihapus dari dashboard.</p></div><button className="cms-button secondary" type="button" disabled={exporting||loading} onClick={()=>void exportCsv()}>{exporting&&<span className="button-spinner"/>}{exporting?"Menyiapkan…":"Ekspor CSV"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    <div className="audit-stats"><div><span>Seluruh catatan</span><strong>{summary.total}</strong></div><div><span>Aktivitas 24 jam</span><strong>{summary.lastDay}</strong></div><div><span>Login gagal 24 jam</span><strong>{summary.loginFailed}</strong></div><div><span>Perubahan konten 24 jam</span><strong>{summary.contentChanges}</strong></div></div>
    <form className="audit-filters" onSubmit={applySearch}><label className="audit-search">Cari aktivitas<input className="cms-input" type="search" value={query} onChange={event=>setQuery(event.target.value)} placeholder="Pengguna, aktivitas, atau ID data"/></label><label>Kategori<select className="cms-input" value={category} onChange={event=>{beginReload();setCategory(event.target.value);setPage(1)}}><option value="all">Semua kategori</option><option value="security">Keamanan & login</option><option value="content">Konten website</option><option value="commercial">Lead, harga & promo</option><option value="settings">Pengaturan</option></select></label><label>Jenis data<select className="cms-input" value={entityType} onChange={event=>{beginReload();setEntityType(event.target.value);setPage(1)}}><option value="">Semua jenis</option>{(data?.filters.entityTypes??[]).map(item=><option value={item} key={item}>{auditEntityNames[item]||item.replaceAll("_"," ")}</option>)}</select></label><label>Pengguna<select className="cms-input" value={userId} onChange={event=>{beginReload();setUserId(event.target.value);setPage(1)}}><option value="">Semua pengguna</option><option value="system">Sistem / tidak dikenal</option>{(data?.filters.users??[]).map(item=><option value={item.id} key={item.id}>{item.name} · {item.email}</option>)}</select></label><label>Dari tanggal<input className="cms-input" type="date" value={dateFrom} onChange={event=>{beginReload();setDateFrom(event.target.value);setPage(1)}}/></label><label>Sampai tanggal<input className="cms-input" type="date" value={dateTo} onChange={event=>{beginReload();setDateTo(event.target.value);setPage(1)}}/></label><div className="audit-filter-actions"><button className="cms-button" type="submit">Terapkan pencarian</button><button className="cms-button secondary" type="button" onClick={resetFilters}>Reset</button></div></form>
    {loading?<div className="lead-loading"><span className="button-spinner"/> Memuat riwayat aktivitas…</div>:data?.logs.length?<div className="audit-list">{data.logs.map(log=>{const isOpen=openId===log.id;return <article className={`audit-item tone-${auditTone(log.action)} ${isOpen?"open":""}`} key={log.id}><button className="audit-summary" type="button" onClick={()=>setOpenId(isOpen?null:log.id)} aria-expanded={isOpen}><span className="audit-event-icon"><Icon name={log.action.startsWith("login")||log.action.startsWith("password")?"shield":log.action.includes("archived")?"close":"check"}/></span><span><small>Aktivitas</small><strong>{auditActionLabel(log.action)}</strong><em>{log.action}</em></span><span><small>Pengguna</small><strong>{log.actorName}</strong><em>{log.actorEmail||"Tanpa akun teridentifikasi"}</em></span><span><small>Data</small><strong>{auditEntityNames[log.entityType]||log.entityType.replaceAll("_"," ")}</strong><em>{log.entityId||"—"}</em></span><span><small>Waktu</small><strong>{auditDate(log.createdAt)}</strong></span><b aria-hidden="true">{isOpen?"−":"+"}</b></button>{isOpen&&<div className="audit-detail"><div className="audit-detail-meta"><div><small>Perangkat</small><strong>{auditDeviceLabel(log.userAgent)}</strong></div><div><small>ID catatan</small><strong>{log.id}</strong></div><div><small>ID data</small><strong>{log.entityId||"Tidak ada"}</strong></div></div><div className="audit-snapshots"><section><h3>Sebelum perubahan</h3><pre>{auditSnapshot(log.before)}</pre></section><section><h3>Sesudah perubahan</h3><pre>{auditSnapshot(log.after)}</pre></section></div><p className="audit-privacy-note"><Icon name="shield"/> Password, token, secret MFA, hash, dan identitas jaringan disembunyikan dari tampilan.</p></div>}</article>})}</div>:<div className="empty-state"><h2>Belum ada aktivitas yang cocok</h2><p>Ubah filter atau pencarian. Aktivitas baru akan tercatat otomatis ketika CMS digunakan.</p><button className="cms-button secondary" type="button" onClick={resetFilters}>Reset filter</button></div>}
    {!loading&&pagination.total>0&&<div className="audit-pagination"><span>Menampilkan {(pagination.page-1)*pagination.perPage+1}–{Math.min(pagination.page*pagination.perPage,pagination.total)} dari {pagination.total} catatan</span><div><button className="cms-button secondary" type="button" disabled={pagination.page<=1} onClick={()=>{beginReload();setPage(current=>Math.max(1,current-1))}}>← Sebelumnya</button><strong>Halaman {pagination.page} / {pagination.totalPages}</strong><button className="cms-button secondary" type="button" disabled={pagination.page>=pagination.totalPages} onClick={()=>{beginReload();setPage(current=>Math.min(pagination.totalPages,current+1))}}>Berikutnya →</button></div></div>}
  </section>
}

function SecurityManager(){
  const [data,setData]=useState<SecurityResponse|null>(null),[loading,setLoading]=useState(true),[reloadKey,setReloadKey]=useState(0),[actionId,setActionId]=useState<string|null>(null),[confirmAction,setConfirmAction]=useState<string|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{let cancelled=false;fetch("/api/cms/security",{cache:"no-store"}).then(async response=>{const json=await response.json() as SecurityResponse;if(!response.ok)throw new Error(json.message||"Data keamanan gagal dimuat.");if(!cancelled)setData(json)}).catch(error=>{if(!cancelled)setNotice({type:"error",message:error instanceof Error?error.message:"Data keamanan gagal dimuat."})}).finally(()=>{if(!cancelled)setLoading(false)});return()=>{cancelled=true}},[reloadKey]);
  function refresh(){setLoading(true);setNotice({type:"info",message:"Memperbarui status keamanan…"});setReloadKey(current=>current+1)}
  async function runAction(action:"terminate_session"|"terminate_other_sessions"|"release_login_locks",sessionId?:string){const key=sessionId||action;setActionId(key);setNotice({type:"info",message:action==="release_login_locks"?"Melepas pembatasan login…":"Memutus sesi dengan aman…"});try{const response=await fetch("/api/cms/security",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action,sessionId})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Tindakan keamanan gagal.");setConfirmAction(null);setNotice({type:"success",message:`✓ ${json.message}`});setLoading(true);setReloadKey(current=>current+1)}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Tindakan keamanan gagal."})}finally{setActionId(null)}}
  const summary=data?.summary??{activeSessions:0,failedLogins24h:0,successfulLogins24h:0,lockedSources:0,mfaAdmins:0,mfaAdminsTotal:0},needsAttention=summary.lockedSources>0||summary.failedLogins24h>=5||summary.mfaAdmins<summary.mfaAdminsTotal;
  const confirmation=confirmAction?confirmAction==="other"?{title:"Putus semua sesi lain?",text:"Semua pengguna di perangkat lain harus login kembali.",action:"terminate_other_sessions" as const,id:undefined}:confirmAction==="locks"?{title:"Lepas pembatasan login?",text:"Penghitung percobaan gagal 15 menit terakhir akan dibersihkan. Riwayat Audit Log tetap tersimpan.",action:"release_login_locks" as const,id:undefined}:{title:"Putus sesi pengguna ini?",text:"Pengguna tersebut harus login kembali untuk membuka CMS.",action:"terminate_session" as const,id:confirmAction}:null;
  return <section className="cms-panel security-manager">
    <div className="cms-panel-header"><div><h2>Pusat Keamanan</h2><p>Pantau perlindungan login, sesi aktif, dan aktivitas mencurigakan dari satu halaman. Hanya Super Admin yang dapat mengakses modul ini.</p></div><div className="security-header-actions"><button className="cms-button secondary" type="button" disabled={loading||actionId!==null} onClick={refresh}>{loading&&<span className="button-spinner"/>}{loading?"Memeriksa…":"Segarkan status"}</button><button className="cms-button danger" type="button" disabled={loading||actionId!==null||summary.activeSessions<=1} onClick={()=>setConfirmAction("other")}>Putus semua sesi lain</button></div></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    {confirmation&&<div className="security-confirm" role="alert"><span className="security-confirm-icon"><Icon name="shield"/></span><div><strong>{confirmation.title}</strong><p>{confirmation.text}</p></div><button className="cms-button secondary" type="button" disabled={actionId!==null} onClick={()=>setConfirmAction(null)}>Batal</button><button className="cms-button danger" type="button" disabled={actionId!==null} onClick={()=>void runAction(confirmation.action,confirmation.id)}>{actionId&&<span className="button-spinner"/>}{actionId?"Memproses…":"Ya, lanjutkan"}</button></div>}
    {loading&&!data?<div className="lead-loading"><span className="button-spinner"/> Memeriksa keamanan CMS…</div>:<>
      <div className={`security-health ${needsAttention?"attention":"healthy"}`}><span className="security-health-icon"><Icon name={needsAttention?"shield":"check"}/></span><div><strong>{needsAttention?"Ada aktivitas yang perlu diperiksa":"Perlindungan utama aktif"}</strong><p>{needsAttention?"Periksa percobaan login, sumber yang dibatasi, dan status MFA di bawah.":"MFA, pembatasan login, sesi aman, validasi permintaan, serta header keamanan terpasang."}</p></div><span>{needsAttention?"Perlu perhatian":"Terlindungi"}</span></div>
      <div className="security-stats"><div><span>Sesi aktif</span><strong>{summary.activeSessions}</strong><small>Termasuk sesi ini</small></div><div><span>Login berhasil 24 jam</span><strong>{summary.successfulLogins24h}</strong><small>Berhasil diverifikasi</small></div><div className={summary.failedLogins24h?"has-warning":""}><span>Login gagal 24 jam</span><strong>{summary.failedLogins24h}</strong><small>Percobaan ditolak</small></div><div className={summary.lockedSources?"has-danger":""}><span>Sumber sedang dibatasi</span><strong>{summary.lockedSources}</strong><small>Penguncian sementara</small></div></div>
      <div className="security-main-grid"><section className="security-subpanel"><div className="security-section-heading"><div><span className="eyebrow">Status perlindungan</span><h3>Pemeriksaan sistem</h3></div><span className="security-count">{data?.protections.filter(item=>item.healthy).length??0}/{data?.protections.length??0} aktif</span></div><div className="security-protection-list">{data?.protections.map(item=><div className={item.healthy?"is-healthy":"needs-review"} key={item.key}><span><Icon name={item.healthy?"check":"shield"}/></span><div><strong>{item.label}</strong><small>{item.detail}</small></div><b>{item.healthy?"Aktif":"Periksa"}</b></div>)}</div></section><section className="security-subpanel security-actions-panel"><div className="security-section-heading"><div><span className="eyebrow">Tindakan cepat</span><h3>Kontrol keamanan</h3></div></div><div className="security-action-card"><span><Icon name="shield"/></span><div><strong>Putus sesi perangkat lain</strong><p>Gunakan jika perangkat hilang, dipinjam, atau aksesnya tidak lagi dipercaya.</p></div><button className="cms-button secondary" type="button" disabled={summary.activeSessions<=1||actionId!==null} onClick={()=>setConfirmAction("other")}>Putus sesi lain</button></div><div className="security-action-card"><span><Icon name="close"/></span><div><strong>Lepas penguncian login</strong><p>Gunakan setelah memastikan percobaan login berasal dari pengguna yang sah.</p></div><button className="cms-button secondary" type="button" disabled={summary.lockedSources===0||actionId!==null} onClick={()=>setConfirmAction("locks")}>{summary.lockedSources?"Lepas pembatasan":"Tidak ada yang terkunci"}</button></div><p className="security-privacy"><Icon name="shield"/> Token sesi, secret MFA, password, dan identitas jaringan tidak pernah ditampilkan.</p></section></div>
      <section className="security-subpanel security-sessions"><div className="security-section-heading"><div><span className="eyebrow">Sesi login</span><h3>Perangkat yang sedang aktif</h3></div><span className="security-count">{data?.sessions.length??0} sesi</span></div>{data?.sessions.length?<div className="security-session-list">{data.sessions.map(session=><article className={session.isCurrent?"is-current":""} key={session.id}><span className="security-device-icon"><Icon name="shield"/></span><div className="security-session-user"><strong>{session.userName}</strong><small>{session.userEmail}</small></div><div><small>Perangkat</small><strong>{auditDeviceLabel(session.userAgent)}</strong></div><div><small>Login</small><strong>{auditDate(session.createdAt)}</strong></div><div><small>Berakhir</small><strong>{auditDate(session.expiresAt)}</strong></div>{session.isCurrent?<span className="security-current-badge">Sesi saat ini</span>:<button className="cms-button danger" type="button" disabled={actionId!==null} onClick={()=>setConfirmAction(session.id)}>Putus sesi</button>}</article>)}</div>:<div className="empty-state"><h2>Tidak ada sesi aktif</h2><p>Sesi yang sudah berakhir tidak ditampilkan.</p></div>}</section>
      <section className="security-subpanel security-events"><div className="security-section-heading"><div><span className="eyebrow">7 hari terakhir</span><h3>Aktivitas keamanan terbaru</h3></div><span className="security-count">{data?.events.length??0} catatan</span></div>{data?.events.length?<div className="security-event-list">{data.events.map(event=><article className={event.action.includes("failed")?"is-danger":""} key={event.id}><span><Icon name={event.action.includes("failed")?"close":"check"}/></span><div><strong>{auditActionLabel(event.action)}</strong><small>{event.actorName}{event.actorEmail?` · ${event.actorEmail}`:""}</small></div><div><small>{auditDeviceLabel(event.userAgent)}</small><strong>{auditDate(event.createdAt)}</strong></div></article>)}</div>:<div className="empty-state"><h2>Belum ada aktivitas keamanan</h2><p>Login, logout, reset password, dan tindakan keamanan akan dicatat otomatis.</p></div>}</section>
    </>}
  </section>
}

function backupSize(bytes:number){if(bytes<1024)return`${bytes} B`;if(bytes<1024*1024)return`${(bytes/1024).toFixed(1)} KB`;return`${(bytes/1024/1024).toFixed(2)} MB`}
function backupStatus(status:string){return status==="ready"?"Tersedia":status==="expired"?"Kedaluwarsa":status==="deleted"?"Dihapus":status.replaceAll("_"," ")}

function BackupManager(){
  const [data,setData]=useState<BackupResponse|null>(null),[loading,setLoading]=useState(true),[reloadKey,setReloadKey]=useState(0),[running,setRunning]=useState<string|null>(null),[note,setNote]=useState(""),[openId,setOpenId]=useState<string|null>(null),[deleteId,setDeleteId]=useState<string|null>(null),[restore,setRestore]=useState<{id:string;confirmation:string;totp:string}|null>(null),[notice,setNotice]=useState<{type:"success"|"error"|"info";message:string}|null>(null);
  useEffect(()=>{let cancelled=false;fetch("/api/cms/backups",{cache:"no-store"}).then(async response=>{const json=await response.json() as BackupResponse;if(!response.ok)throw new Error(json.message||"Backup gagal dimuat.");if(!cancelled)setData(json)}).catch(error=>{if(!cancelled)setNotice({type:"error",message:error instanceof Error?error.message:"Backup gagal dimuat."})}).finally(()=>{if(!cancelled)setLoading(false)});return()=>{cancelled=true}},[reloadKey]);
  function reload(){setLoading(true);setReloadKey(current=>current+1)}
  async function createBackup(){setRunning("create");setNotice({type:"info",message:"Mengumpulkan dan mengenkripsi database…"});try{const response=await fetch("/api/cms/backups",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action:"create",note})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Backup gagal dibuat.");setNote("");setNotice({type:"success",message:`✓ ${json.message}`});reload()}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Backup gagal dibuat."})}finally{setRunning(null)}}
  async function verifyBackup(item:BackupRecord){setRunning(`verify:${item.id}`);setNotice({type:"info",message:`Memverifikasi enkripsi dan integritas ${item.filename}…`});try{const response=await fetch("/api/cms/backups",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action:"verify",id:item.id})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Verifikasi backup gagal.");setNotice({type:"success",message:`✓ ${json.message}`});reload()}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Verifikasi backup gagal."})}finally{setRunning(null)}}
  async function downloadBackup(item:BackupRecord){setRunning(`download:${item.id}`);setNotice({type:"info",message:`Menyiapkan ${item.filename}…`});try{const response=await fetch(`/api/cms/backups?download=${encodeURIComponent(item.id)}`,{cache:"no-store"});if(!response.ok){const json=await response.json() as {message?:string};throw new Error(json.message||"Backup gagal diunduh.")}const blob=await response.blob(),url=URL.createObjectURL(blob),link=document.createElement("a");link.href=url;link.download=item.filename;document.body.appendChild(link);link.click();link.remove();URL.revokeObjectURL(url);setNotice({type:"success",message:`✓ ${item.filename} berhasil diunduh.`})}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Backup gagal diunduh."})}finally{setRunning(null)}}
  async function restoreBackup(event:FormEvent<HTMLFormElement>){event.preventDefault();if(!restore)return;setRunning(`restore:${restore.id}`);setNotice({type:"info",message:"Memverifikasi MFA, membuat backup pengaman, lalu memulihkan database…"});try{const response=await fetch("/api/cms/backups",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({action:"restore",...restore})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Database gagal dipulihkan.");setRestore(null);setNotice({type:"success",message:`✓ ${json.message}`});reload()}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Database gagal dipulihkan."})}finally{setRunning(null)}}
  async function deleteBackup(item:BackupRecord){setRunning(`delete:${item.id}`);setNotice({type:"info",message:`Menghapus ${item.filename}…`});try{const response=await fetch("/api/cms/backups",{method:"DELETE",headers:{"content-type":"application/json"},body:JSON.stringify({id:item.id,confirmation:"HAPUS"})}),json=await response.json() as {message?:string};if(!response.ok)throw new Error(json.message||"Backup gagal dihapus.");setDeleteId(null);setOpenId(null);setNotice({type:"success",message:`✓ ${json.message}`});reload()}catch(error){setNotice({type:"error",message:error instanceof Error?error.message:"Backup gagal dihapus."})}finally{setRunning(null)}}
  const summary=data?.summary??{available:0,totalSize:0,lastBackupAt:"",totalRestores:0,retentionDays:30};
  return <section className="cms-panel backup-manager">
    <div className="cms-panel-header"><div><h2>Backup & Restore Database</h2><p>Buat salinan terenkripsi untuk konten, harga, layanan, pengaturan, leads, navigasi, dan metadata media.</p></div><button className="cms-button secondary" type="button" disabled={loading||running!==null} onClick={()=>{setNotice({type:"info",message:"Memperbarui riwayat backup…"});reload()}}>{loading&&<span className="button-spinner"/>}{loading?"Memuat…":"Segarkan"}</button></div>
    {notice&&<p className={`form-message ${notice.type}`} role="status" aria-live="polite">{notice.message}</p>}
    <div className="backup-stats"><div><span>Backup tersedia</span><strong>{summary.available}</strong><small>Belum kedaluwarsa</small></div><div><span>Penyimpanan terpakai</span><strong>{backupSize(summary.totalSize)}</strong><small>Paket database terenkripsi</small></div><div><span>Backup terakhir</span><strong>{summary.lastBackupAt?auditDate(summary.lastBackupAt):"Belum ada"}</strong><small>Retensi {summary.retentionDays} hari</small></div><div><span>Total pemulihan</span><strong>{summary.totalRestores}</strong><small>Tercatat di Audit Log</small></div></div>
    <div className="backup-main-grid"><section className="backup-create-panel"><div className="backup-create-icon"><Icon name="upload"/></div><div><span className="eyebrow">Backup manual</span><h3>Buat salinan database sekarang</h3><p>Sistem mengenkripsi paket menggunakan AES-256-GCM dan menyimpannya terpisah dari database utama.</p><label>Catatan backup <input className="cms-input" value={note} onChange={event=>setNote(event.target.value)} maxLength={300} placeholder="Contoh: sebelum mengubah harga Agustus"/></label></div><button className="cms-button" type="button" disabled={running!==null||loading} onClick={()=>void createBackup()}>{running==="create"&&<span className="button-spinner"/>}{running==="create"?"Mengenkripsi…":"Buat backup sekarang"}</button></section><section className="backup-coverage"><div className="security-section-heading"><div><span className="eyebrow">Cakupan & perlindungan</span><h3>Yang dilindungi</h3></div></div><ul><li><span><Icon name="check"/></span><div><strong>Database konten dan bisnis</strong><small>Halaman, artikel, harga, layanan, leads, menu, dan pengaturan.</small></div></li><li><span><Icon name="check"/></span><div><strong>Akun tetap aman</strong><small>Password, MFA, sesi, Audit Log, dan riwayat backup tidak ditimpa saat restore.</small></div></li><li><span><Icon name="check"/></span><div><strong>Backup pengaman otomatis</strong><small>Dibuat sebelum setiap proses pemulihan.</small></div></li><li className="is-note"><span><Icon name="image"/></span><div><strong>File gambar tidak diduplikasi</strong><small>Metadata media dibackup; berkas asli tetap berada di Media Library.</small></div></li></ul></section></div>
    {restore&&<form className="backup-restore-panel" onSubmit={event=>void restoreBackup(event)}><span className="backup-restore-icon"><Icon name="shield"/></span><div><span className="eyebrow">Konfirmasi berlapis</span><h3>Pulihkan database dari backup?</h3><p>Konten database saat ini akan diganti dengan isi backup. Sistem membuat backup pengaman terlebih dahulu, sedangkan akun, sesi, MFA, dan Audit Log tidak berubah.</p><div className="backup-restore-fields"><label>Ketik PULIHKAN<input className="cms-input" required value={restore.confirmation} onChange={event=>setRestore(current=>current?{...current,confirmation:event.target.value}:current)} autoComplete="off"/></label><label>Kode MFA terbaru<input className="cms-input" required value={restore.totp} onChange={event=>setRestore(current=>current?{...current,totp:event.target.value.replace(/\D/g,"").slice(0,6)}:current)} inputMode="numeric" pattern="[0-9]{6}" maxLength={6} autoComplete="one-time-code"/></label></div></div><div className="backup-restore-actions"><button className="cms-button secondary" type="button" disabled={running!==null} onClick={()=>setRestore(null)}>Batal</button><button className="cms-button danger" type="submit" disabled={running!==null||restore.confirmation!=="PULIHKAN"||restore.totp.length!==6}>{running===`restore:${restore.id}`&&<span className="button-spinner"/>}{running===`restore:${restore.id}`?"Memulihkan…":"Pulihkan database"}</button></div></form>}
    <section className="backup-history"><div className="security-section-heading"><div><span className="eyebrow">Riwayat backup</span><h3>Backup database tersimpan</h3></div><span className="security-count">{data?.backups.length??0} catatan</span></div>{loading&&!data?<div className="lead-loading"><span className="button-spinner"/> Memuat backup…</div>:data?.backups.length?<div className="backup-list">{data.backups.map(item=>{const isOpen=openId===item.id,available=item.status==="ready";return <article className={`${isOpen?"open":""} status-${item.status}`} key={item.id}><button className="backup-summary" type="button" onClick={()=>setOpenId(isOpen?null:item.id)} aria-expanded={isOpen}><span className="backup-file-icon"><Icon name="upload"/></span><span><small>Nama berkas</small><strong>{item.filename}</strong><em>{item.note||"Tanpa catatan"}</em></span><span><small>Isi backup</small><strong>{item.recordCount.toLocaleString("id-ID")} data</strong><em>{item.tableCount} tabel</em></span><span><small>Ukuran</small><strong>{backupSize(item.size)}</strong><em>AES-256-GCM</em></span><span><small>Dibuat</small><strong>{auditDate(item.createdAt)}</strong><em>oleh {item.createdBy}</em></span><i className={`backup-status status-${item.status}`}>{backupStatus(item.status)}</i><b aria-hidden="true">{isOpen?"−":"+"}</b></button>{isOpen&&<div className="backup-detail"><div className="backup-detail-grid"><div><small>Kedaluwarsa</small><strong>{item.expiresAt?auditDate(item.expiresAt):"—"}</strong></div><div><small>Verifikasi terakhir</small><strong>{item.lastVerifiedAt?auditDate(item.lastVerifiedAt):"Belum diverifikasi"}</strong></div><div><small>Pemulihan terakhir</small><strong>{item.lastRestoredAt?auditDate(item.lastRestoredAt):"Belum pernah"}</strong></div><div><small>Jumlah pemulihan</small><strong>{item.restoreCount} kali</strong></div></div><div className="backup-actions"><button className="cms-button secondary" type="button" disabled={!available||running!==null} onClick={()=>void downloadBackup(item)}>{running===`download:${item.id}`&&<span className="button-spinner"/>}{running===`download:${item.id}`?"Mengunduh…":"Unduh"}</button><button className="cms-button secondary" type="button" disabled={!available||running!==null} onClick={()=>void verifyBackup(item)}>{running===`verify:${item.id}`&&<span className="button-spinner"/>}{running===`verify:${item.id}`?"Memverifikasi…":"Verifikasi"}</button><button className="cms-button" type="button" disabled={!available||running!==null} onClick={()=>{setRestore({id:item.id,confirmation:"",totp:""});setDeleteId(null)}}>Pulihkan</button>{deleteId===item.id?<><span>Hapus backup ini secara permanen?</span><button className="cms-button secondary" type="button" disabled={running!==null} onClick={()=>setDeleteId(null)}>Batal</button><button className="cms-button danger" type="button" disabled={running!==null} onClick={()=>void deleteBackup(item)}>{running===`delete:${item.id}`&&<span className="button-spinner"/>}{running===`delete:${item.id}`?"Menghapus…":"Ya, hapus"}</button></>:<button className="cms-button danger" type="button" disabled={running!==null} onClick={()=>{setDeleteId(item.id);setRestore(null)}}>Hapus</button>}</div></div>}</article>})}</div>:<div className="empty-state"><h2>Belum ada backup database</h2><p>Buat backup pertama sebelum melakukan perubahan besar pada konten, harga, atau konfigurasi website.</p><button className="cms-button" type="button" disabled={running!==null} onClick={()=>void createBackup()}>Buat backup pertama</button></div>}</section>
    <p className="backup-footnote"><Icon name="shield"/> Backup berusia lebih dari 30 hari dibersihkan saat backup baru dibuat. Simpan salinan unduhan di lokasi aman jika membutuhkan retensi lebih panjang.</p>
  </section>
}

function UserManager(){const [status,setStatus]=useState("");async function create(e:FormEvent<HTMLFormElement>){e.preventDefault();const f=new FormData(e.currentTarget);const res=await fetch("/api/cms/users",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(Object.fromEntries(f))});const json=await res.json() as {message:string};setStatus(json.message);if(res.ok)e.currentTarget.reset()}return <section className="cms-panel"><div className="cms-panel-header"><div><h2>User & Role</h2><p>Buat akun Penulis Artikel atau Sales Promotion dengan hak akses minimum.</p></div></div><form className="form-grid" onSubmit={create}><label>Nama<input className="cms-input" required name="name"/></label><label>Email<input className="cms-input" required type="email" name="email"/></label><label>Role<select className="cms-input" name="role"><option value="article_writer">Penulis Artikel</option><option value="sales_promotion">Sales Promotion</option></select></label><label>Password awal<input className="cms-input" required type="password" name="password" minLength={12}/></label><button className="cms-button" type="submit">Buat user</button></form>{status&&<p className="form-message">{status}</p>}</section>}
function ModulePlaceholder({name}:{name:string}){return <section className="cms-panel"><div className="cms-panel-header"><div><h2>{name}</h2><p>Modul ini menggunakan tabel dan hak akses server-side yang telah disiapkan. Konten kosong tidak ditampilkan di frontend.</p></div><button className="cms-button">Tambah data</button></div><div className="empty-state"><h2>Belum ada data</h2><p>Tambahkan data terverifikasi melalui modul ini. Penghapusan konten penting menggunakan arsip/soft delete.</p></div></section>}
