qudoco-fe/components/main/content-website-approver.tsx

334 lines
10 KiB
TypeScript
Raw Normal View History

2026-02-27 08:52:08 +00:00
"use client";
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";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent } from "@/components/ui/card";
import { ExternalLink, Filter, Loader2 } from "lucide-react";
import {
approveCmsContentSubmission,
listCmsContentSubmissions,
rejectCmsContentSubmission,
} from "@/service/cms-content-submissions";
import { apiPayload } from "@/service/cms-landing";
import { formatDate } from "@/utils/global";
import Swal from "sweetalert2";
import ContentWebsite from "@/components/main/content-website";
const DOMAIN_LABEL: Record<string, string> = {
hero: "Hero",
about: "About Us",
product: "Product",
service: "Service",
partner: "Partner",
popup: "Pop Up",
};
/** 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",
};
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() {
const [rows, setRows] = useState<Row[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [actingId, setActingId] = useState<string | null>(null);
const [tabFocusSignal, setTabFocusSignal] = useState(0);
const [tabFocusTarget, setTabFocusTarget] = useState("hero");
const [liveDataReloadSignal, setLiveDataReloadSignal] = useState(0);
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
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" });
});
}
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,
});
setLiveDataReloadSignal((n) => n + 1);
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 (
<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>
<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>
<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
<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>
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell
colSpan={5}
className="text-center text-slate-500 py-10"
>
Tidak ada pengajuan tertunda.
</TableCell>
</TableRow>
) : (
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>
);
}