update flow approve and kontributor
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Anang Yusman 2026-02-27 16:52:08 +08:00
parent 0430d15fd2
commit 96e8538e6a
8 changed files with 528 additions and 56 deletions

View File

@ -1,15 +1,20 @@
"use client";
import Cookies from "js-cookie";
import ContentWebsite from "@/components/main/content-website";
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import ApproverContentWebsite from "@/components/main/content-website-approver";
export default function ContentWebsitePage() {
const [mounted, setMounted] = useState(false);
const [levelId, setLevelId] = useState<string | undefined>();
useEffect(() => {
setMounted(true);
const ulne = Cookies.get("ulne");
setLevelId(ulne);
}, []);
if (!mounted) {
@ -28,7 +33,7 @@ export default function ContentWebsitePage() {
transition={{ duration: 0.3 }}
>
<div className="p-6">
<ContentWebsite />
{levelId === "2" ? <ApproverContentWebsite /> : <ContentWebsite />}
</div>
</motion.div>
);

View File

@ -161,6 +161,13 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
const [startTimeValue, setStartTimeValue] = useState<string>("");
const [levelId, setLevelId] = useState<string | undefined>();
useEffect(() => {
const ulne = Cookies.get("ulne");
setLevelId(ulne);
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles((prevFiles) => [
@ -333,7 +340,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
return;
}
successSubmit("/admin/article");
successSubmit("/admin/news-article/image");
};
const publishScheduled = async () => {
@ -391,7 +398,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
return;
}
successSubmit("/admin/article");
successSubmit("/admin/news-article/image");
};
const save = async (values: z.infer<typeof createArticleSchema>) => {
@ -521,7 +528,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
return;
}
successSubmit("/admin/article");
successSubmit("/admin/news-article/image");
}
});
};
@ -793,18 +800,65 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
<Mail size={20} />
<p className="text-sm ">Suggestion Box (0)</p>
</div>
<div className="border p-3 border-black rounded-lg space-y-2 ">
<h2 className="text-sm text-black font-semibold">
Description :
</h2>
{detailData?.isPublish === true ? (
<span className="inline-block bg-green-100 text-green-700 text-xs font-semibold px-3 py-1 rounded-full">
Approved
</span>
) : (
<span className="inline-block bg-yellow-100 text-yellow-700 text-xs font-semibold px-3 py-1 rounded-full">
Pending
</span>
)}
<p className="text-sm text-black font-semibold">Comment</p>
<h2 className="text-blue-600 text-xs">View Approver History</h2>
</div>
</div>
{/* Action Button */}
<div className="flex justify-end">
<Link href="/admin/news-article/image">
<Button
variant={"outline"}
className="px-6 py-2 rounded-lg transition"
>
Kembali
</Button>
</Link>
{/* ================= ACTION BUTTON ================= */}
<div className="space-y-3">
{levelId === "2" && !detailData?.isPublish && (
<>
<Button
onClick={doPublish}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-xl"
>
Approve
</Button>
<Button
onClick={() => {
setApprovalStatus(4);
doApproval();
}}
className="w-full bg-orange-500 hover:bg-orange-600 text-white py-3 rounded-xl"
>
Revision
</Button>
<Button
onClick={() => {
setApprovalStatus(5);
doApproval();
}}
className="w-full bg-red-600 hover:bg-red-700 text-white py-3 rounded-xl"
>
Reject
</Button>
</>
)}
{/* 🔥 Jika levelId 3 → hanya tampilkan Cancel */}
{levelId === "3" && (
<Link href="/admin/news-article/image">
<Button variant="outline" className="w-full py-3 rounded-xl">
Cancel
</Button>
</Link>
)}
</div>
</div>
</div>

View File

@ -49,7 +49,7 @@ const masterUserSchema = z.object({
genderType: z.string().min(1, { message: "Required" }),
phoneNumber: z.string().min(1, { message: "Required" }),
address: z.string().min(1, { message: "Required" }),
userLevelType: userSchema,
// userLevelType: userSchema,
userRoleType: userSchema,
});
@ -98,7 +98,7 @@ export default function FormMasterUser() {
identityNumber: data.identityNumber,
identityType: "nrp",
phoneNumber: data.phoneNumber,
userLevelId: data.userLevelType.id,
userLevelId: 3,
userRoleId: data.userRoleType.id,
username: data.username,
};
@ -206,6 +206,7 @@ export default function FormMasterUser() {
close();
if (res?.data?.data) {
setupParent(res?.data?.data, "role");
console.log("role", res?.data?.data);
}
};
@ -408,7 +409,7 @@ export default function FormMasterUser() {
{errors.phoneNumber?.message}
</p>
)}
<Controller
{/* <Controller
control={control}
name="userLevelType"
render={({ field: { onChange, value } }) => (
@ -438,7 +439,7 @@ export default function FormMasterUser() {
<p className="text-red-400 text-sm">
{errors.userLevelType?.message}
</p>
)}
)} */}
<Controller
control={control}
name="userRoleType"

View File

@ -29,8 +29,8 @@ interface SidebarSection {
children?: SidebarItem[];
}
const getSidebarByRole = (roleId: string | null) => {
if (roleId === "1") {
const getSidebarByLevel = (levelId: string | null) => {
if (levelId === "1") {
return [
{
title: "Dashboard",
@ -64,7 +64,7 @@ const getSidebarByRole = (roleId: string | null) => {
];
}
if (roleId === "2" || roleId === "3") {
if (levelId === "3") {
return [
{
title: "Dashboard",
@ -141,7 +141,66 @@ const getSidebarByRole = (roleId: string | null) => {
];
}
// fallback kalau role tidak dikenal
if (levelId === "2") {
return [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
items: [
{
title: "Content Website",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/content-website",
},
],
},
{
title: "News & Article",
items: [
{
title: "News & Article",
icon: () => (
<Icon icon="grommet-icons:article" className="text-lg" />
),
children: [
{
title: "Text",
icon: () => <Icon icon="mdi:file-document-outline" />,
link: "/admin/news-article/text",
},
{
title: "Image",
icon: () => <Icon icon="mdi:image-outline" />,
link: "/admin/news-article/image",
},
{
title: "Video",
icon: () => <Icon icon="mdi:video-outline" />,
link: "/admin/news-article/video",
},
{
title: "Audio",
icon: () => <Icon icon="mdi:music-note-outline" />,
link: "/admin/news-article/audio",
},
],
},
],
},
];
}
// fallback kalau Level tidak dikenal
return [];
};
@ -260,7 +319,7 @@ const SidebarContent = ({
const { theme, toggleTheme } = useTheme();
const [username, setUsername] = useState<string>("Guest");
const [roleId, setRoleId] = useState<string | null>(null);
const [LevelId, setLevelId] = useState<string | null>(null);
const [openMenus, setOpenMenus] = useState<string[]>([]);
// ===============================
@ -275,10 +334,10 @@ const SidebarContent = ({
};
const cookieUsername = getCookie("username");
const cookieRoleId = getCookie("urie");
const cookieLevelId = getCookie("ulne");
if (cookieUsername) setUsername(cookieUsername);
if (cookieRoleId) setRoleId(cookieRoleId);
if (cookieLevelId) setLevelId(cookieLevelId);
}, []);
// ===============================
@ -298,7 +357,7 @@ const SidebarContent = ({
);
};
const sidebarSections = getSidebarByRole(roleId);
const sidebarSections = getSidebarByLevel(LevelId);
return (
<div className="flex flex-col h-full">

View File

@ -0,0 +1,149 @@
"use client";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Eye, Pencil, Trash2, Filter } from "lucide-react";
export default function ApproverContentWebsite() {
const tabs = [
"Hero Section",
"About Us",
"Our Products",
"Our Services",
"Technology Partners",
"Pop Up",
];
const data = [
{
title: "Beyond Expectations to Build Reputation.",
subtitle: "-",
author: "John Kontributor",
status: "Published",
date: "2024-01-15",
},
{
title: "Manajemen Reputasi untuk Institusi",
subtitle: "-",
author: "Sarah Kontributor",
status: "Pending",
date: "2024-01-14",
},
];
return (
<div className="space-y-6">
{/* HEADER */}
<div>
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
<p className="text-slate-500">
Update homepage content, products, services, and partners
</p>
</div>
{/* TABS */}
<div className="bg-white rounded-2xl shadow border p-2 flex flex-wrap gap-2">
{tabs.map((tab, i) => (
<button
key={i}
className={`px-4 py-2 rounded-xl text-sm font-medium transition ${
i === 0
? "bg-blue-600 text-white"
: "hover:bg-slate-100 text-slate-600"
}`}
>
{tab}
</button>
))}
</div>
{/* SEARCH & FILTER */}
<div className="flex gap-4">
<Input placeholder="Search Hero Section by title, author, or content..." />
<Button variant="outline" className="flex items-center gap-2">
<Filter size={16} /> Filters
</Button>
</div>
{/* TABLE */}
<div className="bg-white rounded-2xl shadow border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr>
<th className="text-left px-6 py-4">Main Title</th>
<th className="text-left px-6 py-4">Subtitle</th>
<th className="text-left px-6 py-4">Author</th>
<th className="text-left px-6 py-4">Status</th>
<th className="text-left px-6 py-4">Date</th>
<th className="text-left px-6 py-4">Actions</th>
</tr>
</thead>
<tbody>
{data.map((item, i) => (
<tr key={i} className="border-t hover:bg-slate-50 transition">
<td className="px-6 py-4 font-medium text-slate-800">
{item.title}
</td>
<td className="px-6 py-4">{item.subtitle}</td>
<td className="px-6 py-4 text-slate-600">{item.author}</td>
<td className="px-6 py-4">
<span
className={`text-xs font-medium px-3 py-1 rounded-full ${
item.status === "Published"
? "bg-green-100 text-green-600"
: "bg-yellow-100 text-yellow-600"
}`}
>
{item.status}
</span>
</td>
<td className="px-6 py-4 text-slate-600">{item.date}</td>
<td className="px-6 py-4">
<div className="flex gap-3 text-slate-500">
<Eye
size={18}
className="cursor-pointer hover:text-blue-600"
/>
<Pencil
size={18}
className="cursor-pointer hover:text-green-600"
/>
<Trash2
size={18}
className="cursor-pointer hover:text-red-600"
/>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* FOOTER */}
<div className="flex justify-between items-center px-6 py-4 border-t bg-slate-50">
<p className="text-sm text-slate-500">Showing 1 to 2 of 2 items</p>
<div className="flex gap-2">
<button className="px-4 py-2 border rounded-lg text-sm">
Previous
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm">
1
</button>
<button className="px-4 py-2 border rounded-lg text-sm">2</button>
<button className="px-4 py-2 border rounded-lg text-sm">3</button>
<button className="px-4 py-2 border rounded-lg text-sm">
Next
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -52,10 +52,10 @@ interface PostCount {
}
export default function DashboardContainer() {
const [roleName, setRoleName] = useState<string | undefined>();
const [levelName, setLevelName] = useState<string | undefined>();
useEffect(() => {
const role = Cookies.get("roleName");
setRoleName(role);
const levelId = Cookies.get("ulne");
setLevelName(levelId);
}, []);
const username = Cookies.get("username");
@ -135,7 +135,7 @@ export default function DashboardContainer() {
return month + " " + year;
};
if (!roleName) return null;
if (!levelName) return null;
const AdminDashboard = () => {
const tasks = [
{
@ -494,7 +494,199 @@ export default function DashboardContainer() {
);
};
const ApproverDashboard = () => {
const stats = [
{
title: "Pending Review",
value: 12,
growth: "+3",
color: "bg-yellow-500",
},
{
title: "Approved Today",
value: 8,
growth: "+5",
color: "bg-green-600",
},
{
title: "Total Published",
value: 156,
growth: "+12%",
color: "bg-blue-600",
},
{
title: "Rejected",
value: 5,
growth: "-1",
color: "bg-red-600",
},
];
const pendingList = [
{
title: "MediaHUB Content Aggregator",
author: "John Kontributor",
category: "Product",
time: "2 hours ago",
status: "Pending",
},
{
title: "Artifintel Services Update",
author: "John Kontributor",
category: "Service",
time: "2 hours ago",
status: "Pending",
},
];
const activities = [
{
status: "Approved",
title: "Technology Summit Event",
time: "10 mins ago",
},
{
status: "Rejected",
title: "Product Update Draft",
time: "25 mins ago",
},
{
status: "Approved",
title: "Partner Logo Update",
time: "1 hour ago",
},
];
return (
<div className="space-y-8">
{/* HEADER */}
<div>
<h1 className="text-2xl font-bold text-slate-800">
Approver Dashboard
</h1>
<p className="text-slate-500">
Review and manage content submissions
</p>
</div>
{/* ================= STAT CARDS ================= */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((card, i) => (
<div
key={i}
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-start"
>
<div>
<p className="text-sm text-slate-500">{card.title}</p>
<h2 className="text-3xl font-bold text-slate-800 mt-2">
{card.value}
</h2>
</div>
<div className="text-right">
<p className="text-sm text-green-600 font-medium">
{card.growth}
</p>
<div className={`w-10 h-10 rounded-xl mt-3 ${card.color}`} />
</div>
</div>
))}
</div>
{/* ================= CONTENT SECTION ================= */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* LEFT - Pending Review */}
<div className="lg:col-span-2 bg-white rounded-2xl shadow border p-6 space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">
Pending Review{" "}
<span className="ml-2 text-xs bg-amber-100 text-amber-600 px-3 py-1 rounded-full">
{pendingList.length} Items
</span>
</h2>
<button className="text-blue-600 text-sm font-medium">
View All
</button>
</div>
{pendingList.map((item, i) => (
<div
key={i}
className="border border-amber-300 bg-amber-50 rounded-xl p-4 space-y-4"
>
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-slate-800">
{item.title}
</h4>
<p className="text-sm text-slate-500 mt-1">
{item.author} {item.category} {item.time}
</p>
</div>
<span className="text-xs bg-amber-200 text-amber-700 px-3 py-1 rounded-full">
{item.status}
</span>
</div>
<div className="flex gap-4">
<button className="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 rounded-lg text-sm font-medium">
Approve
</button>
<button className="flex-1 bg-red-600 hover:bg-red-700 text-white py-2 rounded-lg text-sm font-medium">
Reject
</button>
<button className="px-4 py-2 bg-gray-200 rounded-lg text-sm">
Review
</button>
</div>
</div>
))}
</div>
{/* RIGHT - Recent Activity */}
<div className="bg-white rounded-2xl shadow border p-6 space-y-6">
<h2 className="text-lg font-semibold">Recent Activity</h2>
<div className="space-y-4">
{activities.map((item, i) => (
<div
key={i}
className="border rounded-xl p-4 flex justify-between items-center"
>
<div>
<p
className={`text-sm font-medium ${
item.status === "Approved"
? "text-green-600"
: "text-red-600"
}`}
>
{item.status}
</p>
<p className="text-sm text-slate-700">{item.title}</p>
<p className="text-xs text-slate-500">{item.time}</p>
</div>
</div>
))}
</div>
<button className="w-full bg-[#966314] hover:bg-[#7a4f0f] text-white py-3 rounded-xl text-sm font-medium">
View All Activity
</button>
</div>
</div>
</div>
);
};
return (
<>{roleName === "Admin" ? <AdminDashboard /> : <ContributorDashboard />}</>
<>
{levelName === "1" && <AdminDashboard />}
{levelName === "3" && <ContributorDashboard />}
{levelName === "2" && <ApproverDashboard />}
</>
);
}

View File

@ -18,12 +18,19 @@ import Link from "next/link";
import { getArticlePagination } from "@/service/article";
import { formatDate } from "@/utils/global";
import { close, loading } from "@/config/swal";
import Cookies from "js-cookie";
export default function NewsImage() {
const [articles, setArticles] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [search, setSearch] = useState("");
const [levelId, setLevelId] = useState<string | undefined>();
useEffect(() => {
const ulne = Cookies.get("ulne");
setLevelId(ulne);
}, []);
useEffect(() => {
fetchData();
@ -52,18 +59,21 @@ export default function NewsImage() {
}
const statusVariant = (status: string) => {
switch (status?.toLowerCase()) {
case "publish":
return "bg-green-100 text-green-700";
case "pending":
return "bg-yellow-100 text-yellow-700";
case "draft":
return "bg-gray-200 text-gray-600";
case "reject":
return "bg-red-100 text-red-600";
default:
return "bg-gray-200 text-gray-600";
const value = status?.toLowerCase();
if (value === "published") {
return "bg-green-100 text-green-700";
}
if (value === "pending") {
return "bg-yellow-100 text-yellow-700";
}
if (value === "cancel") {
return "bg-red-100 text-red-700";
}
return "bg-gray-200 text-gray-600";
};
return (
@ -78,12 +88,14 @@ export default function NewsImage() {
Create and manage news articles and blog posts
</p>
</div>
<Link href={"/admin/news-article/image/create"}>
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
<Plus className="w-4 h-4 mr-2" />
Create New Article
</Button>
</Link>
{levelId === "3" && (
<Link href={"/admin/news-article/image/create"}>
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
<Plus className="w-4 h-4 mr-2" />
Create New Article
</Button>
</Link>
)}
</div>
{/* ================= SEARCH ================= */}

View File

@ -30,7 +30,7 @@ export async function getListArticle(props: PaginationRequest) {
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
sort || "desc"
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
null
null,
);
}
@ -50,7 +50,7 @@ export async function getArticlePagination(props: PaginationRequest) {
source,
} = props;
return await httpGetInterceptor(
return await httpGet(
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
startDate || ""
}&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
@ -59,7 +59,7 @@ export async function getArticlePagination(props: PaginationRequest) {
sortBy || "created_at"
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
isBanner || ""
}`
}`,
);
}
@ -75,7 +75,7 @@ export async function getTopArticles(props: PaginationRequest) {
}&title=${search}&startDate=${startDate || ""}&endDate=${
endDate || ""
}&category=${category || ""}&sortBy=view_count&sort=desc`,
headers
headers,
);
}
@ -121,11 +121,11 @@ export async function deleteArticle(id: string) {
}
export async function getArticleByCategory() {
return await httpGetInterceptor(`/article-categories?limit=1000`);
return await httpGet(`/article-categories?limit=1000`);
}
export async function getCategoryPagination(data: any) {
return await httpGet(
`/article-categories?limit=${data?.limit}&page=${data?.page}&title=${data?.search}`
`/article-categories?limit=${data?.limit}&page=${data?.page}&title=${data?.search}`,
);
}
@ -159,7 +159,7 @@ export async function deleteArticleFiles(id: number) {
export async function getUserLevelDataStat(startDate: string, endDate: string) {
return await httpGet(
`/articles/statistic/user-levels?startDate=${startDate}&endDate=${endDate}`
`/articles/statistic/user-levels?startDate=${startDate}&endDate=${endDate}`,
);
}
export async function getStatisticMonthly(year: string) {