2026-02-27 08:52:08 +00:00
|
|
|
"use client";
|
|
|
|
|
|
2026-04-13 17:47:40 +00:00
|
|
|
import { useCallback, useEffect, useState } from "react";
|
2026-02-27 08:52:08 +00:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-04-13 17:47:40 +00:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from "@/components/ui/table";
|
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
2026-04-14 03:27:00 +00:00
|
|
|
import { ExternalLink, Filter, Loader2 } from "lucide-react";
|
2026-04-13 17:47:40 +00:00
|
|
|
import {
|
|
|
|
|
approveCmsContentSubmission,
|
|
|
|
|
listCmsContentSubmissions,
|
|
|
|
|
rejectCmsContentSubmission,
|
|
|
|
|
} from "@/service/cms-content-submissions";
|
|
|
|
|
import { apiPayload } from "@/service/cms-landing";
|
|
|
|
|
import { formatDate } from "@/utils/global";
|
|
|
|
|
import Swal from "sweetalert2";
|
2026-04-14 03:27:00 +00:00
|
|
|
import ContentWebsite from "@/components/main/content-website";
|
2026-04-13 17:47:40 +00:00
|
|
|
|
|
|
|
|
const DOMAIN_LABEL: Record<string, string> = {
|
|
|
|
|
hero: "Hero",
|
|
|
|
|
about: "About Us",
|
|
|
|
|
product: "Product",
|
|
|
|
|
service: "Service",
|
|
|
|
|
partner: "Partner",
|
|
|
|
|
popup: "Pop Up",
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-14 03:27:00 +00:00
|
|
|
/** Maps submission `domain` to ContentWebsite tab `value`. */
|
|
|
|
|
const DOMAIN_TO_TAB: Record<string, string> = {
|
|
|
|
|
hero: "hero",
|
|
|
|
|
about: "about",
|
|
|
|
|
product: "products",
|
|
|
|
|
service: "services",
|
|
|
|
|
partner: "partners",
|
|
|
|
|
popup: "popup",
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-13 17:47:40 +00:00
|
|
|
type Row = {
|
|
|
|
|
id: string;
|
|
|
|
|
domain: string;
|
|
|
|
|
title: string;
|
|
|
|
|
status: string;
|
|
|
|
|
submitter_name: string;
|
|
|
|
|
submitted_by_id: number;
|
|
|
|
|
payload: string;
|
|
|
|
|
created_at: string;
|
|
|
|
|
};
|
2026-02-27 08:52:08 +00:00
|
|
|
|
|
|
|
|
export default function ApproverContentWebsite() {
|
2026-04-13 17:47:40 +00:00
|
|
|
const [rows, setRows] = useState<Row[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [actingId, setActingId] = useState<string | null>(null);
|
2026-04-14 03:27:00 +00:00
|
|
|
const [tabFocusSignal, setTabFocusSignal] = useState(0);
|
|
|
|
|
const [tabFocusTarget, setTabFocusTarget] = useState("hero");
|
|
|
|
|
const [liveDataReloadSignal, setLiveDataReloadSignal] = useState(0);
|
2026-04-13 17:47:40 +00:00
|
|
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await listCmsContentSubmissions({
|
|
|
|
|
status: "pending",
|
|
|
|
|
page: 1,
|
|
|
|
|
limit: 100,
|
|
|
|
|
});
|
|
|
|
|
const raw = apiPayload<Row[]>(res);
|
|
|
|
|
setRows(Array.isArray(raw) ? raw : []);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
load();
|
|
|
|
|
}, [load]);
|
|
|
|
|
|
|
|
|
|
const filtered = rows.filter((r) => {
|
|
|
|
|
const q = search.trim().toLowerCase();
|
|
|
|
|
if (!q) return true;
|
|
|
|
|
return (
|
|
|
|
|
r.title.toLowerCase().includes(q) ||
|
|
|
|
|
(r.submitter_name ?? "").toLowerCase().includes(q) ||
|
|
|
|
|
(DOMAIN_LABEL[r.domain] ?? r.domain).toLowerCase().includes(q)
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-02-27 08:52:08 +00:00
|
|
|
|
2026-04-14 03:27:00 +00:00
|
|
|
function jumpToLiveTab(domain: string) {
|
|
|
|
|
const tab = DOMAIN_TO_TAB[domain];
|
|
|
|
|
if (!tab) return;
|
|
|
|
|
setTabFocusTarget(tab);
|
|
|
|
|
setTabFocusSignal((n) => n + 1);
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
document
|
|
|
|
|
.getElementById("approver-live-cms")
|
|
|
|
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:47:40 +00:00
|
|
|
async function onApprove(id: string) {
|
|
|
|
|
const ok = await Swal.fire({
|
|
|
|
|
icon: "question",
|
|
|
|
|
title: "Terapkan perubahan ke website?",
|
|
|
|
|
showCancelButton: true,
|
|
|
|
|
});
|
|
|
|
|
if (!ok.isConfirmed) return;
|
|
|
|
|
setActingId(id);
|
|
|
|
|
try {
|
|
|
|
|
const res = await approveCmsContentSubmission(id);
|
|
|
|
|
if ((res as { error?: boolean })?.error) {
|
|
|
|
|
await Swal.fire({
|
|
|
|
|
icon: "error",
|
|
|
|
|
title: "Gagal",
|
|
|
|
|
text: String((res as { message?: unknown })?.message ?? ""),
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await Swal.fire({
|
|
|
|
|
icon: "success",
|
|
|
|
|
title: "Disetujui",
|
|
|
|
|
timer: 1600,
|
|
|
|
|
showConfirmButton: false,
|
|
|
|
|
});
|
2026-04-14 03:27:00 +00:00
|
|
|
setLiveDataReloadSignal((n) => n + 1);
|
2026-04-13 17:47:40 +00:00
|
|
|
await load();
|
|
|
|
|
} finally {
|
|
|
|
|
setActingId(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onReject(id: string) {
|
|
|
|
|
const note = await Swal.fire({
|
|
|
|
|
icon: "warning",
|
|
|
|
|
title: "Tolak pengajuan?",
|
|
|
|
|
input: "textarea",
|
|
|
|
|
inputPlaceholder: "Catatan (opsional)",
|
|
|
|
|
showCancelButton: true,
|
|
|
|
|
});
|
|
|
|
|
if (!note.isConfirmed) return;
|
|
|
|
|
setActingId(id);
|
|
|
|
|
try {
|
|
|
|
|
const res = await rejectCmsContentSubmission(
|
|
|
|
|
id,
|
|
|
|
|
typeof note.value === "string" ? note.value : "",
|
|
|
|
|
);
|
|
|
|
|
if ((res as { error?: boolean })?.error) {
|
|
|
|
|
await Swal.fire({
|
|
|
|
|
icon: "error",
|
|
|
|
|
title: "Gagal",
|
|
|
|
|
text: String((res as { message?: unknown })?.message ?? ""),
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await Swal.fire({
|
|
|
|
|
icon: "success",
|
|
|
|
|
title: "Ditolak",
|
|
|
|
|
timer: 1400,
|
|
|
|
|
showConfirmButton: false,
|
|
|
|
|
});
|
|
|
|
|
await load();
|
|
|
|
|
} finally {
|
|
|
|
|
setActingId(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-27 08:52:08 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-04-14 03:27:00 +00:00
|
|
|
<div className="space-y-10">
|
2026-02-27 08:52:08 +00:00
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
|
2026-04-14 03:27:00 +00:00
|
|
|
<p className="mt-1 text-slate-500">
|
|
|
|
|
Di bagian atas: pengajuan baru yang perlu disetujui. Di bawah: konten
|
|
|
|
|
live di semua tab (hanya lihat) untuk membandingkan dengan pengajuan.
|
2026-02-27 08:52:08 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-14 03:27:00 +00:00
|
|
|
<section className="space-y-4">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
<h2 className="text-lg font-semibold text-slate-900">
|
|
|
|
|
Perubahan menunggu persetujuan
|
|
|
|
|
</h2>
|
|
|
|
|
<Badge variant="secondary" className="font-normal">
|
|
|
|
|
{filtered.length} pending
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-slate-500">
|
|
|
|
|
Setujui atau tolak di sini. Gunakan{" "}
|
|
|
|
|
<span className="font-medium text-slate-700">Lihat konten live</span>{" "}
|
|
|
|
|
untuk membuka tab yang sama dengan bagian yang diajukan.
|
|
|
|
|
</p>
|
2026-02-27 08:52:08 +00:00
|
|
|
|
2026-04-14 03:27:00 +00:00
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Cari judul, bagian, atau nama pengaju…"
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
className="max-w-md"
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="gap-2 shrink-0"
|
|
|
|
|
onClick={() => load()}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Filter className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
Refresh
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card className="rounded-2xl border shadow-sm overflow-hidden">
|
|
|
|
|
<CardContent className="p-0">
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex justify-center py-16">
|
|
|
|
|
<Loader2 className="h-10 w-10 animate-spin text-[#966314]" />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow className="bg-slate-50">
|
|
|
|
|
<TableHead>Judul</TableHead>
|
|
|
|
|
<TableHead>Bagian</TableHead>
|
|
|
|
|
<TableHead>Pengaju</TableHead>
|
|
|
|
|
<TableHead>Tanggal</TableHead>
|
|
|
|
|
<TableHead className="text-right w-[280px]">Aksi</TableHead>
|
2026-04-13 17:47:40 +00:00
|
|
|
</TableRow>
|
2026-04-14 03:27:00 +00:00
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{filtered.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell
|
|
|
|
|
colSpan={5}
|
|
|
|
|
className="text-center text-slate-500 py-10"
|
|
|
|
|
>
|
|
|
|
|
Tidak ada pengajuan tertunda.
|
2026-04-13 17:47:40 +00:00
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
2026-04-14 03:27:00 +00:00
|
|
|
) : (
|
|
|
|
|
filtered.map((item) => (
|
|
|
|
|
<TableRow key={item.id}>
|
|
|
|
|
<TableCell className="font-medium max-w-xs">
|
|
|
|
|
{item.title}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
{DOMAIN_LABEL[item.domain] ?? item.domain}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-slate-600">
|
|
|
|
|
{item.submitter_name ||
|
|
|
|
|
`User #${item.submitted_by_id}`}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-slate-600">
|
|
|
|
|
{formatDate(item.created_at)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
<div className="flex flex-wrap justify-end gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="gap-1"
|
|
|
|
|
onClick={() => jumpToLiveTab(item.domain)}
|
|
|
|
|
>
|
|
|
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
|
|
|
Lihat konten live
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="bg-green-600 hover:bg-green-700"
|
|
|
|
|
disabled={actingId === item.id}
|
|
|
|
|
onClick={() => onApprove(item.id)}
|
|
|
|
|
>
|
|
|
|
|
Setujui
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="text-red-600 border-red-200"
|
|
|
|
|
disabled={actingId === item.id}
|
|
|
|
|
onClick={() => onReject(item.id)}
|
|
|
|
|
>
|
|
|
|
|
Tolak
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section
|
|
|
|
|
id="approver-live-cms"
|
|
|
|
|
className="space-y-4 border-t border-slate-200 pt-10 scroll-mt-6"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-lg font-semibold text-slate-900">
|
|
|
|
|
Konten live (semua tab, hanya lihat)
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="mt-1 text-sm text-slate-500">
|
|
|
|
|
Data yang sedang ditampilkan di website. Field tidak dapat diubah di
|
|
|
|
|
sini — bandingkan dengan baris pengajuan di atas sebelum
|
|
|
|
|
menyetujui.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<ContentWebsite
|
|
|
|
|
viewOnly
|
|
|
|
|
hideHeader
|
|
|
|
|
tabFocusSignal={tabFocusSignal}
|
|
|
|
|
tabFocusTarget={tabFocusTarget}
|
|
|
|
|
liveDataReloadSignal={liveDataReloadSignal}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
2026-02-27 08:52:08 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|