251 lines
7.3 KiB
TypeScript
251 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
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 { 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";
|
|
|
|
const DOMAIN_LABEL: Record<string, string> = {
|
|
hero: "Hero",
|
|
about: "About Us",
|
|
product: "Product",
|
|
service: "Service",
|
|
partner: "Partner",
|
|
popup: "Pop Up",
|
|
};
|
|
|
|
type Row = {
|
|
id: string;
|
|
domain: string;
|
|
title: string;
|
|
status: string;
|
|
submitter_name: string;
|
|
submitted_by_id: number;
|
|
payload: string;
|
|
created_at: string;
|
|
};
|
|
|
|
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 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)
|
|
);
|
|
});
|
|
|
|
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,
|
|
});
|
|
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);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
|
|
<p className="text-slate-500">
|
|
Tinjau pengajuan perubahan dari kontributor dan terapkan ke konten live.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
<Input
|
|
placeholder="Cari judul, domain, 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">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 space-x-2">
|
|
<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>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|