fix: all section in tenant and others

This commit is contained in:
Sabda Yagra 2025-10-06 21:17:48 +07:00
parent f44a7c4be1
commit e2d0e17846
27 changed files with 1543 additions and 735 deletions

View File

@ -1,11 +1,12 @@
import CategoriesDetailForm from "@/components/form/categories/categories-detail-form"; import CategoriesDetailForm from "@/components/form/categories/categories-detail-form";
import FormImageDetail from "@/components/form/content/image/image-detail-form"; import FormImageDetail from "@/components/form/content/image/image-detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const CategoriesDetailPage = async () => { const CategoriesDetailPage = async () => {
return ( return (
<div> <div>
{/* <SiteBreadcrumb /> */} <SiteBreadcrumb />
<div className="space-y-4"> <div className="space-y-4 bg-slate-100">
<CategoriesDetailForm />{" "} <CategoriesDetailForm />{" "}
</div> </div>
</div> </div>

View File

@ -0,0 +1,16 @@
import CategoriesUpdateForm from "@/components/form/categories/categories-update-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import React from "react";
const page = () => {
return (
<div className="">
<SiteBreadcrumb />
<div className="bg-slate-100">
<CategoriesUpdateForm />
</div>
</div>
);
};
export default page;

View File

@ -17,6 +17,7 @@ import withReactContent from "sweetalert2-react-content";
import { error } from "@/lib/swal"; import { error } from "@/lib/swal";
import Link from "next/link"; import Link from "next/link";
import { deleteMedia } from "@/service/content"; import { deleteMedia } from "@/service/content";
import { deleteArticle } from "@/service/content/content";
const useTableColumns = () => { const useTableColumns = () => {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
@ -55,12 +56,10 @@ const useTableColumns = () => {
const categoryName = row.getValue("categoryName"); const categoryName = row.getValue("categoryName");
const categories = row.original.categories; const categories = row.original.categories;
// Handle new API structure with categories array // Handle new API structure with categories array
const displayName = categoryName || (categories && categories.length > 0 ? categories[0].title : "-"); const displayName =
return ( categoryName ||
<span className="whitespace-nowrap"> (categories && categories.length > 0 ? categories[0].title : "-");
{displayName} return <span className="whitespace-nowrap">{displayName}</span>;
</span>
);
}, },
}, },
{ {
@ -191,13 +190,8 @@ const useTableColumns = () => {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
async function doDelete(id: any) { async function doDelete(id: any) {
// loading(); const data = { id };
const data = { const response = await deleteArticle(id);
id,
};
const response = await deleteMedia(data);
if (response?.error) { if (response?.error) {
error(response.message); error(response.message);
return false; return false;
@ -275,8 +269,8 @@ const useTableColumns = () => {
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
</Link> */} </Link> */}
{(Number(row.original.uploadedById) === Number(userId) || {/* {(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && ( isMabesApprover) && ( */}
<Link <Link
href={`/admin/content/audio-visual/update/${row.original.id}`} href={`/admin/content/audio-visual/update/${row.original.id}`}
> >
@ -285,7 +279,7 @@ const useTableColumns = () => {
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
)} {/* )} */}
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)} onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none" className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"

View File

@ -176,13 +176,12 @@ const TableVideo = () => {
: ""; : "";
try { try {
// Using the new interface-based approach for video content
const filters: ArticleFilters = { const filters: ArticleFilters = {
page: page, page,
totalPage: Number(showData), totalPage: Number(showData),
title: search || undefined, title: search || undefined,
categoryId: categoryFilter ? Number(categoryFilter) : undefined, categoryId: categoryFilter ? Number(categoryFilter) : undefined,
typeId: 2, // video content type typeId: 2, // ✅ untuk video
statusId: statusId:
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined, statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
startDate: formattedStartDate || undefined, startDate: formattedStartDate || undefined,
@ -197,28 +196,28 @@ const TableVideo = () => {
item.no = (page - 1) * Number(showData) + index + 1; item.no = (page - 1) * Number(showData) + index + 1;
}); });
setDataTable(data); setDataTable(data);
setTotalData(data.length); setTotalData(res?.data?.meta?.count || data.length);
setTotalPage(Math.ceil(data.length / Number(showData))); setTotalPage(
Math.ceil((res?.data?.meta?.count || data.length) / Number(showData))
);
} else if (Array.isArray(data?.content)) { } else if (Array.isArray(data?.content)) {
const contentData = data.content; const contentData = data.content;
contentData.forEach((item: any, index: number) => { contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1; item.no = (page - 1) * Number(showData) + index + 1;
}); });
setDataTable(contentData); setDataTable(contentData);
setTotalData(data?.totalElements ?? contentData.length); setTotalData(res?.data?.meta?.count || contentData.length);
setTotalPage( setTotalPage(
data?.totalPages ?? Math.ceil(contentData.length / Number(showData)) Math.ceil(
(res?.data?.meta?.count || contentData.length) / Number(showData)
)
); );
} else { } else {
setDataTable([]); setDataTable([]);
setTotalData(0);
setTotalPage(1);
} }
} catch (error) { } catch (err) {
console.error("Error fetching tasks:", error); console.error("Error fetching tasks:", err);
setDataTable([]); setDataTable([]);
setTotalData(0);
setTotalPage(1);
} }
} }

View File

@ -4,7 +4,8 @@ import SiteBreadcrumb from "@/components/site-breadcrumb";
const VideoCreatePage = async () => { const VideoCreatePage = async () => {
return ( return (
<div> <div>
<div className="space-y-4 m-3"> <SiteBreadcrumb />
<div className="space-y-4 bg-slate-100">
<FormVideo /> <FormVideo />
</div> </div>
</div> </div>

View File

@ -14,11 +14,11 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { format } from "date-fns"; import { format } from "date-fns";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { deleteMedia } from "@/service/content/content";
import { error } from "@/lib/swal"; import { error } from "@/lib/swal";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Link from "next/link"; import Link from "next/link";
import { deleteArticle } from "@/service/content/content";
const useTableColumns = () => { const useTableColumns = () => {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
@ -172,7 +172,7 @@ const useTableColumns = () => {
async function doDelete(id: any) { async function doDelete(id: any) {
const data = { id }; const data = { id };
const response = await deleteMedia(data); const response = await deleteArticle(id);
if (response?.error) { if (response?.error) {
error(response.message); error(response.message);
return false; return false;
@ -240,14 +240,14 @@ const useTableColumns = () => {
View View
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
{(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && ( {/* {(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && ( */}
<Link href={`/admin/content/image/update/${row.original.id}`}> <Link href={`/admin/content/image/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none"> <DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<SquarePen className="w-4 h-4 me-1.5" /> <SquarePen className="w-4 h-4 me-1.5" />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
)} {/* )} */}
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)} onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none" className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"

View File

@ -52,7 +52,6 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination"; import TablePagination from "@/components/table/table-pagination";
import { import {
deleteMedia,
listDataImage, listDataImage,
listArticles, listArticles,
listArticlesWithFilters, listArticlesWithFilters,

View File

@ -4,8 +4,8 @@ import SiteBreadcrumb from "@/components/site-breadcrumb";
const ImageCreatePage = async () => { const ImageCreatePage = async () => {
return ( return (
<div> <div>
{/* <SiteBreadcrumb /> */} <SiteBreadcrumb />
<div className="space-y-4"> <div className="space-y-4 bg-slate-100">
<FormImage /> <FormImage />
</div> </div>
</div> </div>

View File

@ -1,10 +1,11 @@
import FormImageDetail from "@/components/form/content/image/image-detail-form"; import FormImageDetail from "@/components/form/content/image/image-detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const ImageDetailPage = async () => { const ImageDetailPage = async () => {
return ( return (
<div> <div>
{/* <SiteBreadcrumb /> */} <SiteBreadcrumb />
<div className="space-y-4"> <div className="space-y-4 bg-slate-100">
<FormImageDetail /> <FormImageDetail />
</div> </div>
</div> </div>

View File

@ -1,8 +1,10 @@
import FormImageUpdate from "@/components/form/content/image/image-update-form"; import FormImageUpdate from "@/components/form/content/image/image-update-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const ImageUpdatePage = async () => { const ImageUpdatePage = async () => {
return ( return (
<div> <div>
<SiteBreadcrumb />
<div className="space-y-4"> <div className="space-y-4">
<FormImageUpdate /> <FormImageUpdate />
</div> </div>

View File

@ -11,17 +11,21 @@ import {
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { deleteMedia } from "@/service/content/content";
import { error } from "@/lib/swal"; import { error } from "@/lib/swal";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Link from "next/link";
import { deleteUserLevel } from "@/service/tenant"; import { deleteUserLevel } from "@/service/tenant";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { getUserLevelDetail } from "@/service/tenant";
const useTableColumns = () => { const useTableColumns = (onEdit?: (data: any) => void) => {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const userLevelId = getCookiesDecrypt("ulie"); const userLevelId = getCookiesDecrypt("ulie");
@ -185,6 +189,23 @@ const useTableColumns = () => {
cell: ({ row }) => { cell: ({ row }) => {
const router = useRouter(); const router = useRouter();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [detailData, setDetailData] = React.useState<any>(null);
const handleView = async (id: number) => {
try {
const res = await getUserLevelDetail(id);
if (!res?.error) {
setDetailData(res?.data?.data);
setIsDialogOpen(true);
} else {
error(res?.message || "Gagal memuat detail user level");
}
} catch (err) {
console.error("View error:", err);
error("Terjadi kesalahan saat memuat data.");
}
};
async function doDelete(id: number) { async function doDelete(id: number) {
const response = await deleteUserLevel(id); const response = await deleteUserLevel(id);
@ -242,7 +263,6 @@ const useTableColumns = () => {
const userId = getCookiesDecrypt("uie"); const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie"); const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie"); const roleId = getCookiesDecrypt("urie");
React.useEffect(() => { React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) { if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover( setIsMabesApprover(
@ -251,43 +271,91 @@ const useTableColumns = () => {
} }
}, [userLevelId, roleId]); }, [userLevelId, roleId]);
const [open, setOpen] = React.useState(false);
return ( return (
<DropdownMenu> <DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
size="icon" size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent" className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
> >
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" /> <MoreVertical className="h-4 w-4 text-default-800" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end"> <DropdownMenuContent className="p-0 hover:text-black" align="end">
{/* <Link
href={`/admin/settings/tenant/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link> */}
{/* {(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && ( */}
<Link href={`/admin/settings/tenant/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
{/* )} */}
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)} onClick={() => {
setOpen(false);
handleView(row.original.id);
}}
className="p-2 border-b text-default-700 rounded-none cursor-pointer"
>
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOpen(false); // ⬅️ tutup dropdown manual
onEdit?.(row.original);
}}
className="p-2 border-b text-default-700 rounded-none cursor-pointer"
>
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOpen(false); // ⬅️ pastikan dropdown tertutup sebelum alert
handleDeleteMedia(row.original.id);
}}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none" className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
> >
<Trash2 className="w-4 h-4 me-1.5" /> <Trash2 className="w-4 h-4 me-1.5" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
{/* ✅ Dialog Detail User Level */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Detail User Level</DialogTitle>
<DialogDescription>
Informasi lengkap dari user level ID: {detailData?.id}
</DialogDescription>
</DialogHeader>
{detailData ? (
<div className="space-y-3 mt-4">
<p>
<span className="font-medium">Name:</span>{" "}
{detailData.aliasName}
</p>
<p>
<span className="font-medium">Group:</span>{" "}
{detailData.group}
</p>
<p>
<span className="font-medium">Parent Level:</span>{" "}
{detailData.parentLevelName || "-"}
</p>
<p>
<span className="font-medium">Created At:</span>{" "}
{detailData.createdAt
? new Date(detailData.createdAt).toLocaleString("id-ID")
: "-"}
</p>
</div>
) : (
<p className="text-gray-500 mt-4">Memuat data...</p>
)}
<div className="flex justify-end mt-5">
<Button onClick={() => setIsDialogOpen(false)}>Tutup</Button>
</div>
</DialogContent>
</Dialog>
</DropdownMenu> </DropdownMenu>
); );
}, },

View File

@ -48,6 +48,9 @@ import {
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import TablePagination from "@/components/table/table-pagination"; import TablePagination from "@/components/table/table-pagination";
import useTableColumns from "./columns"; import useTableColumns from "./columns";
import TenantUpdateForm from "@/components/form/tenant/tenant-update-form";
import { errorAutoClose, successAutoClose } from "@/lib/swal";
import { close, loading } from "@/config/swal";
function TenantSettingsContentTable() { function TenantSettingsContentTable() {
const [activeTab, setActiveTab] = useState("workflows"); const [activeTab, setActiveTab] = useState("workflows");
@ -59,6 +62,14 @@ function TenantSettingsContentTable() {
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false); const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
const { checkWorkflowStatus } = useWorkflowStatusCheck(); const { checkWorkflowStatus } = useWorkflowStatusCheck();
const { showWorkflowModal } = useWorkflowModal(); const { showWorkflowModal } = useWorkflowModal();
const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false);
const [editingUserLevel, setEditingUserLevel] =
React.useState<UserLevel | null>(null);
const handleEditUserLevel = (userLevel: UserLevel) => {
setEditingUserLevel(userLevel);
setIsEditDialogOpen(true);
};
React.useEffect(() => { React.useEffect(() => {
loadData(); loadData();
@ -90,6 +101,8 @@ function TenantSettingsContentTable() {
}); });
setUserLevels(data); setUserLevels(data);
console.log("LLL", data); console.log("LLL", data);
setTotalData(data.length);
setTotalPage(Math.ceil(data.length / Number(showData)));
} }
} catch (error) { } catch (error) {
console.error("Error loading data:", error); console.error("Error loading data:", error);
@ -107,19 +120,29 @@ function TenantSettingsContentTable() {
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => { const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
try { try {
loading();
const response = await createUserLevel(data); const response = await createUserLevel(data);
close();
if (response?.error) { if (response?.error) {
console.error("Error creating user level:", response?.message); errorAutoClose(response.message || "Gagal membuat user level.");
} else { return;
console.log("User level created successfully:", response);
} }
successAutoClose("User level berhasil dibuat.");
setIsUserLevelDialogOpen(false);
setTimeout(async () => {
await loadData();
}, 3000);
} catch (error) { } catch (error) {
close();
errorAutoClose("Terjadi kesalahan saat membuat user level.");
console.error("Error creating user level:", error); console.error("Error creating user level:", error);
} }
setIsUserLevelDialogOpen(false);
await loadData();
}; };
const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => { const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => {
@ -127,7 +150,10 @@ function TenantSettingsContentTable() {
await loadData(); await loadData();
}; };
const columns = useTableColumns(); const columns = React.useMemo(
() => useTableColumns((data) => handleEditUserLevel(data)),
[]
);
const [showData, setShowData] = React.useState("10"); const [showData, setShowData] = React.useState("10");
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1); const [totalPage, setTotalPage] = React.useState(1);
@ -759,7 +785,7 @@ function TenantSettingsContentTable() {
)} )}
<Table className="overflow-hidden mt-3 mx-3"> <Table className="overflow-hidden mt-3 mx-3">
<TableHeader> <TableHeader className="sticky top-0 bg-white shadow-sm z-10">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200"> <TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
@ -805,6 +831,45 @@ function TenantSettingsContentTable() {
)} )}
</TableBody> </TableBody>
</Table> </Table>
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="md:max-w-6xl max-h-[90vh] overflow-y-auto transition-all duration-200 ease-in-out">
<DialogHeader>
<DialogTitle>
{editingUserLevel
? `Edit User Level: ${editingUserLevel.name}`
: "Edit User Level"}
</DialogTitle>
</DialogHeader>
{editingUserLevel ? (
<TenantUpdateForm
id={editingUserLevel.id}
initialData={{
name: editingUserLevel.name,
aliasName: editingUserLevel.aliasName,
levelNumber: editingUserLevel.levelNumber,
parentLevelId: editingUserLevel.parentLevelId || 0,
provinceId: editingUserLevel.provinceId || 0,
group: editingUserLevel.group || "",
isApprovalActive: editingUserLevel.isApprovalActive,
isActive: editingUserLevel.isActive,
}}
onSuccess={async () => {
setIsEditDialogOpen(false);
setEditingUserLevel(null);
await loadData();
}}
onCancel={() => {
setIsEditDialogOpen(false);
setEditingUserLevel(null);
}}
/>
) : (
<p className="text-gray-500 text-center py-6">Loading...</p>
)}
</DialogContent>
</Dialog>
<TablePagination <TablePagination
table={table} table={table}
totalData={totalData} totalData={totalData}

View File

@ -1,9 +0,0 @@
import React from 'react'
const page = () => {
return (
<div>page</div>
)
}
export default page

View File

@ -44,28 +44,21 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
isActive: true, isActive: true,
}); });
// Bulk form state
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]); const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
// API data
const [userLevels, setUserLevels] = useState<UserLevel[]>([]); const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [provinces, setProvinces] = useState<Province[]>([]); const [provinces, setProvinces] = useState<Province[]>([]);
// UI state
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [expandedHierarchy, setExpandedHierarchy] = useState(false); const [expandedHierarchy, setExpandedHierarchy] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true); const [isLoadingData, setIsLoadingData] = useState(true);
const [activeTab, setActiveTab] = useState(mode === "single" ? "basic" : "bulk"); const [activeTab, setActiveTab] = useState(mode === "single" ? "basic" : "bulk");
// Load initial data
useEffect(() => { useEffect(() => {
if (initialData) { if (initialData) {
setFormData(initialData); setFormData(initialData);
} }
}, [initialData]); }, [initialData]);
// Load API data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
try { try {
@ -86,7 +79,6 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
loadData(); loadData();
}, []); }, []);
// Validation
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => { const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
@ -133,7 +125,6 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
return isValid; return isValid;
}; };
// Form handlers
const handleFieldChange = (field: keyof UserLevelsCreateRequest, value: any) => { const handleFieldChange = (field: keyof UserLevelsCreateRequest, value: any) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) { if (errors[field]) {
@ -151,11 +142,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
setActiveTab(value); setActiveTab(value);
// Update mode based on active tab
if (value === "bulk") { if (value === "bulk") {
// Mode will be determined by activeTab in handleSubmit
} else { } else {
// Mode will be determined by activeTab in handleSubmit
} }
}; };

View File

@ -49,9 +49,9 @@ export default function CategoriesDetailForm() {
return ( return (
<form> <form>
{detail ? ( {detail ? (
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10 border rounded-lg ">
{/* MAIN FORM */} {/* MAIN FORM */}
<Card className="w-full lg:w-8/12 px-6 py-6"> <Card className="w-full lg:w-8/12 px-6 py-6 m-2">
<p className="text-lg font-semibold mb-3">Form Category Detail</p> <p className="text-lg font-semibold mb-3">Form Category Detail</p>
{/* Title */} {/* Title */}
@ -79,7 +79,7 @@ export default function CategoriesDetailForm() {
{/* Thumbnail */} {/* Thumbnail */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<Label>Thumbnail</Label> <Label>Thumbnail</Label>
<Card className="mt-2 w-fit"> <Card className="mt-2 w-fit p-2 border">
<img <img
src={detail.thumbnailUrl} src={detail.thumbnailUrl}
alt="Category Thumbnail" alt="Category Thumbnail"
@ -89,9 +89,9 @@ export default function CategoriesDetailForm() {
</div> </div>
{/* Tags */} {/* Tags */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3 ">
<Label>Tags</Label> <Label>Tags</Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2 p-4 border rounded-sm">
{detail?.tags?.length > 0 ? ( {detail?.tags?.length > 0 ? (
detail.tags.map((tag: string, i: number) => ( detail.tags.map((tag: string, i: number) => (
<Badge key={i} className="px-2 py-1 rounded-md"> <Badge key={i} className="px-2 py-1 rounded-md">
@ -106,7 +106,7 @@ export default function CategoriesDetailForm() {
</Card> </Card>
{/* SIDEBAR */} {/* SIDEBAR */}
<div className="w-full lg:w-4/12"> <div className="w-full lg:w-4/12 m-2">
<Card className="px-4 py-4 space-y-4"> <Card className="px-4 py-4 space-y-4">
{/* Creator */} {/* Creator */}
<div> <div>
@ -155,7 +155,7 @@ export default function CategoriesDetailForm() {
<Button <Button
type="button" type="button"
onClick={() => router.push("/admin/categories")} onClick={() => router.push("/admin/categories")}
className="mt-4" className="mt-4 border"
> >
Back to Categories Back to Categories
</Button> </Button>

View File

@ -0,0 +1,272 @@
"use client";
import React, { useEffect, useState, ChangeEvent } from "react";
import { useParams, useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { formatDateToIndonesian } from "@/utils/globals";
import {
getArticleCategoryDetail,
updateArticleCategory,
uploadArticleCategoryThumbnail,
} from "@/service/categories/categories";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
type CategoryDetail = {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
parentId: number;
createdById: number;
createdByName?: string;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
export default function CategoriesUpdateForm() {
const { id } = useParams() as { id: string };
const router = useRouter();
const MySwal = withReactContent(Swal);
const [formData, setFormData] = useState<CategoryDetail | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
async function init() {
if (id) {
try {
const res = await getArticleCategoryDetail(Number(id));
setFormData(res?.data?.data);
setThumbnailPreview(res?.data?.data?.thumbnailUrl || null);
} catch (err) {
console.error("Error fetching category detail:", err);
}
}
}
init();
}, [id]);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => (prev ? { ...prev, [name]: value } : prev));
};
const handleCheckboxChange = (name: string, value: boolean) => {
setFormData((prev) => (prev ? { ...prev, [name]: value } : prev));
};
const handleThumbnailChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setThumbnailFile(file);
setThumbnailPreview(URL.createObjectURL(file));
};
const handleThumbnailUpload = async (categoryId: number) => {
if (!thumbnailFile) return null;
setIsUploading(true);
try {
const formData = new FormData();
formData.append("files", thumbnailFile); // sesuai Swagger: "files"
const res = await uploadArticleCategoryThumbnail(categoryId, formData);
if (!res?.error) {
MySwal.fire("Sukses", "Thumbnail berhasil diunggah!", "success");
return res?.data?.file_url || null;
} else {
MySwal.fire(
"Error",
res?.message || "Gagal mengunggah thumbnail",
"error"
);
return null;
}
} catch (error) {
console.error("Upload error:", error);
MySwal.fire("Error", "Gagal mengunggah file", "error");
return null;
} finally {
setIsUploading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData) return;
setIsSubmitting(true);
try {
// 🔹 upload thumbnail jika ada
if (thumbnailFile) {
await handleThumbnailUpload(Number(id));
}
// 🔹 hanya kirim data mandatory
const payload = {
id: formData.id,
description: formData.description || "",
statusId: formData.statusId || 1,
title: formData.title || "",
};
const res = await updateArticleCategory(Number(id), payload);
if (!res?.error) {
MySwal.fire({
icon: "success",
title: "Sukses!",
text: "Kategori berhasil diperbarui.",
});
router.push("/admin/categories");
} else {
MySwal.fire({
icon: "error",
title: "Gagal",
text: res.message || "Gagal memperbarui kategori",
});
}
} catch (err) {
console.error("Update error:", err);
MySwal.fire({
icon: "error",
title: "Error!",
text: "Terjadi kesalahan server.",
});
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{formData ? (
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
{/* MAIN FORM */}
<Card className="w-full lg:w-8/12 px-6 py-6 m-2">
<p className="text-lg font-semibold mb-3">Edit Category</p>
{/* Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Input
name="title"
value={formData.title}
onChange={handleChange}
/>
</div>
{/* Description */}
<div className="space-y-2 py-3">
<Label>Description</Label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
className="w-full border rounded-md p-2"
rows={4}
/>
</div>
{/* Thumbnail Upload */}
<div className="space-y-2 py-3">
<Label>Thumbnail</Label>
{thumbnailPreview && (
<img
src={thumbnailPreview}
alt="Preview"
className="h-[180px] w-auto rounded-md border mb-2"
/>
)}
<Input
type="file"
accept="image/*"
onChange={handleThumbnailChange}
/>
{isUploading && (
<p className="text-sm text-blue-500 mt-1">Mengunggah...</p>
)}
</div>
{/* Status */}
<div className="flex items-center gap-4 py-3">
<div className="flex items-center gap-2">
<Checkbox
checked={formData.isActive}
onCheckedChange={(checked) =>
handleCheckboxChange("isActive", Boolean(checked))
}
/>
<Label>Active</Label>
</div>
</div>
<Button
type="submit"
className="w-full mt-4 bg-green-600 text-white"
disabled={isSubmitting || isUploading}
>
{isSubmitting ? "Menyimpan..." : "Simpan Perubahan"}
</Button>
</Card>
{/* SIDEBAR */}
<div className="w-full lg:w-4/12 m-2">
<Card className="px-4 py-4 space-y-4">
<div>
<Label>Created By</Label>
<Input
type="text"
value={formData.createdByName || formData.createdById}
readOnly
/>
</div>
<div>
<Label>Created At</Label>
<p className="text-sm">
{formatDateToIndonesian(new Date(formData.createdAt))}
</p>
</div>
<div>
<Label>Updated At</Label>
<p className="text-sm">
{formatDateToIndonesian(new Date(formData.updatedAt))}
</p>
</div>
<Button
type="button"
variant="outline"
onClick={() => router.push("/admin/categories")}
className="w-full"
>
Kembali
</Button>
</Card>
</div>
</div>
) : (
<p className="text-center text-slate-500">Loading...</p>
)}
</form>
);
}

View File

@ -329,26 +329,30 @@ export default function FormImageDetail() {
const mappedDetail: Detail = { const mappedDetail: Detail = {
...details, ...details,
// Map legacy fields for backward compatibility // Map legacy fields for backward compatibility
category: details.categories && details.categories.length > 0 ? { category:
id: details.categories[0].id, details.categories && details.categories.length > 0
name: details.categories[0].title ? {
} : undefined, id: details.categories[0].id,
name: details.categories[0].title,
}
: undefined,
creatorName: details.createdByName, creatorName: details.createdByName,
thumbnailLink: details.thumbnailUrl, thumbnailLink: details.thumbnailUrl,
statusName: getStatusName(details.statusId), statusName: getStatusName(details.statusId),
needApprovalFromLevel: 0, // This might need to be updated based on your business logic needApprovalFromLevel: 0, // This might need to be updated based on your business logic
uploadedById: details.createdById, uploadedById: details.createdById,
files: details.files || [] files: details.files || [],
}; };
// Map files from new API structure to expected format // Map files from new API structure to expected format
const mappedFiles = (mappedDetail.files || []).map((file: any) => ({ const mappedFiles = (mappedDetail.files || []).map((file: any) => ({
id: file.id, id: file.id,
url: file.fileUrl || file.url, url: file.fileUrl || file.url,
thumbnailFileUrl: file.fileThumbnail || file.thumbnailFileUrl || file.fileUrl, thumbnailFileUrl:
file.fileThumbnail || file.thumbnailFileUrl || file.fileUrl,
fileName: file.fileName || file.fileName, fileName: file.fileName || file.fileName,
// Keep original API fields for reference // Keep original API fields for reference
...file ...file,
})); }));
setFiles(mappedFiles); setFiles(mappedFiles);
@ -367,8 +371,12 @@ export default function FormImageDetail() {
// Set the selected target to the category ID from details // Set the selected target to the category ID from details
setSelectedTarget(String(mappedDetail.categoryId)); setSelectedTarget(String(mappedDetail.categoryId));
const fileUrls = mappedFiles.map((file: any) => const fileUrls = mappedFiles.map(
file.thumbnailFileUrl || file.url || mappedDetail.thumbnailUrl || "default-image.jpg" (file: any) =>
file.thumbnailFileUrl ||
file.url ||
mappedDetail.thumbnailUrl ||
"default-image.jpg"
); );
setDetailThumb(fileUrls); setDetailThumb(fileUrls);
@ -389,15 +397,15 @@ export default function FormImageDetail() {
1: "Menunggu Review", 1: "Menunggu Review",
2: "Diterima", 2: "Diterima",
3: "Minta Update", 3: "Minta Update",
4: "Ditolak" 4: "Ditolak",
}; };
return statusMap[statusId] || "Unknown"; return statusMap[statusId] || "Unknown";
}; };
// Helper function to get file extension // Helper function to get file extension
const getFileExtension = (filename: string): string => { const getFileExtension = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase(); const ext = filename.split(".").pop()?.toLowerCase();
return ext || 'jpg'; return ext || "jpg";
}; };
const actionApproval = (e: string) => { const actionApproval = (e: string) => {
@ -565,8 +573,8 @@ export default function FormImageDetail() {
return ( return (
<form> <form>
{detail !== undefined ? ( {detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Image</p> <p className="text-lg font-semibold mb-3">Form Image</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
@ -611,11 +619,16 @@ export default function FormImageDetail() {
{detail && {detail &&
!categories?.find( !categories?.find(
(cat) => (cat) =>
String(cat.id) === String(detail.categoryId || detail?.category?.id) String(cat.id) ===
String(detail.categoryId || detail?.category?.id)
) && ( ) && (
<SelectItem <SelectItem
key={String(detail.categoryId || detail?.category?.id)} key={String(
value={String(detail.categoryId || detail?.category?.id)} detail.categoryId || detail?.category?.id
)}
value={String(
detail.categoryId || detail?.category?.id
)}
> >
{detail.categoryName || detail?.category?.name} {detail.categoryName || detail?.category?.name}
</SelectItem> </SelectItem>
@ -694,7 +707,7 @@ export default function FormImageDetail() {
</div> </div>
</div> </div>
</Card> </Card>
<div className="w-full lg:w-4/12"> <div className="w-full lg:w-4/12 m-2">
<Card className="pb-3"> <Card className="pb-3">
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="space-y-2"> <div className="space-y-2">
@ -737,7 +750,7 @@ export default function FormImageDetail() {
.map((tag: string, index: number) => ( .map((tag: string, index: number) => (
<Badge <Badge
key={index} key={index}
className="border rounded-md px-2 py-2" className="border rounded-md bg-black text-white px-2 py-2"
> >
{tag.trim()} {tag.trim()}
</Badge> </Badge>
@ -746,13 +759,14 @@ export default function FormImageDetail() {
</div> </div>
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2"> <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Checkbox <Checkbox
id="5" id="5"
checked={selectedPublishers.includes(5)} checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)} onChange={() => handleCheckboxChange(5)}
className="border"
/> />
<Label htmlFor="5">UMUM</Label> <Label htmlFor="5">UMUM</Label>
</div> </div>
@ -761,6 +775,7 @@ export default function FormImageDetail() {
id="6" id="6"
checked={selectedPublishers.includes(6)} checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)} onChange={() => handleCheckboxChange(6)}
className="border"
/> />
<Label htmlFor="6">JOURNALIS</Label> <Label htmlFor="6">JOURNALIS</Label>
</div> </div>
@ -769,6 +784,7 @@ export default function FormImageDetail() {
id="7" id="7"
checked={selectedPublishers.includes(7)} checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)} onChange={() => handleCheckboxChange(7)}
className="border"
/> />
<Label htmlFor="7">POLRI</Label> <Label htmlFor="7">POLRI</Label>
</div> </div>
@ -777,6 +793,7 @@ export default function FormImageDetail() {
id="8" id="8"
checked={selectedPublishers.includes(8)} checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)} onChange={() => handleCheckboxChange(8)}
className="border"
/> />
<Label htmlFor="8">KSP</Label> <Label htmlFor="8">KSP</Label>
</div> </div>
@ -789,7 +806,9 @@ export default function FormImageDetail() {
/> />
<div className="px-3 py-3 border mx-3"> <div className="px-3 py-3 border mx-3">
<p>Information:</p> <p>Information:</p>
<p className="text-sm text-slate-400">{detail?.statusName || getStatusName(detail?.statusId || 0)}</p> <p className="text-sm text-slate-400">
{detail?.statusName || getStatusName(detail?.statusId || 0)}
</p>
<p>Komentar</p> <p>Komentar</p>
<p>{approval?.message}</p> <p>{approval?.message}</p>
<p className="text-right text-sm"> <p className="text-right text-sm">
@ -1082,11 +1101,13 @@ export default function FormImageDetail() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</Card> </Card>
{Number(detail?.needApprovalFromLevel || 0) == Number(userLevelId) || {Number(detail?.needApprovalFromLevel || 0) ==
Number(userLevelId) ||
(detail?.isInternationalMedia == true && (detail?.isInternationalMedia == true &&
detail?.isForwardFromNational == true && detail?.isForwardFromNational == true &&
Number(detail?.statusId) == 1) ? ( Number(detail?.statusId) == 1) ? (
Number(detail?.createdById || detail?.uploadedById) == Number(userId) ? ( Number(detail?.createdById || detail?.uploadedById) ==
Number(userId) ? (
"" ""
) : ( ) : (
<div className="flex flex-col gap-2 p-3"> <div className="flex flex-col gap-2 p-3">

View File

@ -901,8 +901,8 @@ export default function FormImage() {
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Image</p> <p className="text-lg font-semibold mb-3">Form Image</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
@ -1363,7 +1363,7 @@ export default function FormImage() {
</div> </div>
</Card> </Card>
<div className="w-full lg:w-4/12"> <div className="w-full lg:w-4/12 m-2">
<Card className=" h-[500px]"> <Card className=" h-[500px]">
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="space-y-2"> <div className="space-y-2">
@ -1527,6 +1527,7 @@ export default function FormImage() {
id={option.id} id={option.id}
checked={isChecked} checked={isChecked}
onCheckedChange={handleChange} onCheckedChange={handleChange}
className="border"
/> />
<Label htmlFor={option.id}>{option.label}</Label> <Label htmlFor={option.id}>{option.label}</Label>
</div> </div>

View File

@ -25,14 +25,12 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module"; import { register } from "module";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { import {
createMedia, createMedia,
deleteFile, deleteFile,
deleteMedia,
getTagsBySubCategoryId, getTagsBySubCategoryId,
listEnableCategory, listEnableCategory,
uploadThumbnail, uploadThumbnail,
@ -106,7 +104,7 @@ export default function FormImageUpdate() {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter(); const router = useRouter();
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
console.log(id); console.log("INI ID NYA", id);
const editor = useRef(null); const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>; type ImageSchema = z.infer<typeof imageSchema>;
let progressInfo: any = []; let progressInfo: any = [];
@ -677,8 +675,7 @@ export default function FormImageUpdate() {
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{/* Show the category from details if it doesn't exist in categories list */} {detail?.category?.id &&
{detail &&
!categories.find( !categories.find(
(cat) => (cat) =>
String(cat.id) === String(detail.category.id) String(cat.id) === String(detail.category.id)
@ -690,6 +687,7 @@ export default function FormImageUpdate() {
{detail.category.name} {detail.category.name}
</SelectItem> </SelectItem>
)} )}
{categories.map((category) => ( {categories.map((category) => (
<SelectItem <SelectItem
key={String(category.id)} key={String(category.id)}

View File

@ -9,28 +9,21 @@ import { Input } from "../ui/input";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
// ✅ import services
import { requestOTP, createUser } from "@/service/auth"; import { requestOTP, createUser } from "@/service/auth";
export default function SignUp() { export default function SignUp() {
const router = useRouter(); const router = useRouter();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const [step, setStep] = useState<"login" | "otp" | "form">("login"); const [step, setStep] = useState<"login" | "otp" | "form">("login");
const [role, setRole] = useState("umum"); const [role, setRole] = useState("umum");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [otp, setOtp] = useState(["", "", "", "", "", ""]); const [otp, setOtp] = useState(["", "", "", "", "", ""]);
// role-specific
const [membershipType, setMembershipType] = useState(""); const [membershipType, setMembershipType] = useState("");
const [certNumber, setCertNumber] = useState(""); const [certNumber, setCertNumber] = useState("");
const [namaTenant, setNamaTenant] = useState(""); const [namaTenant, setNamaTenant] = useState("");
const [namaPerusahaan, setNamaPerusahaan] = useState(""); const [namaPerusahaan, setNamaPerusahaan] = useState("");
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
// data user lengkap
const [fullname, setFullname] = useState(""); const [fullname, setFullname] = useState("");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [phoneNumber, setPhoneNumber] = useState(""); const [phoneNumber, setPhoneNumber] = useState("");
@ -40,16 +33,21 @@ export default function SignUp() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [workType, setWorkType] = useState(""); const [workType, setWorkType] = useState("");
// 🔹 Kirim OTP
const handleSendOtp = async (e: React.FormEvent) => { const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!email) { if (!email) {
MySwal.fire("Error", "Email wajib diisi", "error"); if (!email) {
MySwal.fire({
icon: "error",
title: "Error",
text: "Email wajib diisi",
});
return;
}
return; return;
} }
// tentukan name sesuai role
let name = ""; let name = "";
if (role === "tenant") name = namaTenant || `${firstName} ${lastName}`; if (role === "tenant") name = namaTenant || `${firstName} ${lastName}`;
else if (role === "kontributor") else if (role === "kontributor")
@ -61,30 +59,48 @@ export default function SignUp() {
const res = await requestOTP({ email, name }); const res = await requestOTP({ email, name });
if (!res?.error) { if (!res?.error) {
MySwal.fire("Sukses", "OTP berhasil dikirim ke email anda", "success"); MySwal.fire({
icon: "success",
title: "Sukses",
text: "OTP berhasil dikirim ke email anda",
});
setStep("otp"); setStep("otp");
} else { } else {
MySwal.fire("Gagal", res.message || "Gagal mengirim OTP", "error"); MySwal.fire({
icon: "error",
title: "Gagal",
text: res.message || "Gagal mengirim OTP",
});
} }
} catch (err) { } catch (err) {
console.error("Error send otp:", err); console.error("Error send otp:", err);
MySwal.fire("Error", "Terjadi kesalahan server", "error"); MySwal.fire({
icon: "error",
title: "Terjadi kesalahan server",
text: "Gagal mengirim OTP",
});
} }
}; };
// 🔹 Verifikasi OTP
const handleVerifyOtp = async (e: React.FormEvent) => { const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const code = otp.join(""); const code = otp.join("");
if (code.length !== 6) { if (code.length !== 6) {
MySwal.fire("Error", "OTP harus 6 digit", "error"); MySwal.fire({
icon: "error",
title: "OTP harus 6 digit",
text: "error",
});
return; return;
} }
MySwal.fire("Sukses", "OTP diverifikasi!", "success"); MySwal.fire({
icon: "success",
title: "Sukses",
text: "OTP diverifikasi!",
});
setStep("form"); setStep("form");
}; };
// 🔹 Register User (API baru)
const handleRegister = async (e: React.FormEvent) => { const handleRegister = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -111,14 +127,26 @@ export default function SignUp() {
try { try {
const res = await createUser(payload); const res = await createUser(payload);
if (!res?.error) { if (!res?.error) {
MySwal.fire("Sukses", "Akun berhasil dibuat!", "success"); MySwal.fire({
icon: "success",
title: "Sukses",
text: "Akun berhasil dibuat!",
});
router.push("/auth"); router.push("/auth");
} else { } else {
MySwal.fire("Error", res.message || "Gagal membuat akun", "error"); MySwal.fire({
icon: "error",
title: "Gagal",
text: res.message || "Gagal membuat akun",
});
} }
} catch (err) { } catch (err) {
console.error("Register error:", err); console.error("Register error:", err);
MySwal.fire("Error", "Terjadi kesalahan server", "error"); MySwal.fire({
icon: "error",
title: "Gagal",
text: "Terjadi kesalahan server",
});
} }
}; };

View File

@ -1,9 +1,197 @@
import React from 'react' "use client";
const TenantUpdateForm = () => { import React, { useEffect, useState } from "react";
return ( import { useForm, Controller } from "react-hook-form";
<div>TenantUpdateForm</div> import { zodResolver } from "@hookform/resolvers/zod";
) import * as z from "zod";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { errorAutoClose, loading, successAutoClose } from "@/lib/swal";
import { close } from "@/config/swal";
import { getUserLevelDetail, updateUserLevel } from "@/service/tenant";
const tenantSchema = z.object({
aliasName: z.string().trim().min(1, { message: "Alias Name wajib diisi" }),
levelNumber: z
.number({
invalid_type_error: "Level Number harus berupa angka",
})
.min(1, { message: "Level Number minimal 1" }),
name: z.string().trim().min(1, { message: "Name wajib diisi" }),
});
type TenantSchema = z.infer<typeof tenantSchema>;
interface TenantUpdateFormProps {
id: number;
/** ✅ Tambahkan properti ini */
initialData?: {
name: string;
aliasName: string;
levelNumber: number;
parentLevelId?: number;
provinceId?: number;
group: string;
isApprovalActive: boolean;
isActive: boolean;
};
onSuccess?: () => void;
onCancel?: () => void;
} }
export default TenantUpdateForm export default function TenantUpdateForm({
id,
onSuccess,
onCancel,
}: TenantUpdateFormProps) {
const MySwal = withReactContent(Swal);
const [loadingData, setLoadingData] = useState(false);
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<TenantSchema>({
resolver: zodResolver(tenantSchema),
defaultValues: {
aliasName: "",
levelNumber: 1,
name: "",
},
});
useEffect(() => {
async function getDetail() {
setLoadingData(true);
try {
const response = await getUserLevelDetail(id);
if (!response.error && response.data) {
const detail = response.data;
setValue("aliasName", detail.aliasName ?? "");
setValue("levelNumber", detail.levelNumber ?? 1);
setValue("name", detail.name ?? "");
} else {
console.error("Gagal mengambil detail:", response.message);
}
} catch (err) {
console.error("Error fetching detail:", err);
} finally {
setLoadingData(false);
}
}
if (id) getDetail();
}, [id, setValue]);
const onSubmit = async (data: TenantSchema) => {
try {
loading();
const payload = {
aliasName: data.aliasName,
levelNumber: data.levelNumber,
name: data.name,
};
console.log("Payload dikirim ke API:", payload);
const response = await updateUserLevel(Number(id), payload);
close();
if (response?.error) {
errorAutoClose(response.message || "Gagal memperbarui data.");
return;
}
successAutoClose("Data berhasil diperbarui.");
setTimeout(() => {
if (onSuccess) onSuccess();
}, 3000);
} catch (err) {
close();
errorAutoClose("Terjadi kesalahan saat menyimpan data.");
console.error("Update user level error:", err);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card className="p-6 w-full lg:w-2/3">
<h2 className="text-lg font-semibold mb-4">
Update Tenant (User Level)
</h2>
<div className="space-y-4">
{/* Alias Name */}
<div>
<Label>Alias Name</Label>
<Controller
control={control}
name="aliasName"
render={({ field }) => (
<Input {...field} placeholder="Masukkan alias name" />
)}
/>
{errors.aliasName && (
<p className="text-red-500 text-sm">{errors.aliasName.message}</p>
)}
</div>
{/* Level Number */}
<div>
<Label>Level Number</Label>
<Controller
control={control}
name="levelNumber"
render={({ field }) => (
<Input
type="number"
{...field}
onChange={(e) => field.onChange(Number(e.target.value) || 1)}
placeholder="Masukkan level number"
/>
)}
/>
{errors.levelNumber && (
<p className="text-red-500 text-sm">
{errors.levelNumber.message}
</p>
)}
</div>
{/* Name */}
<div>
<Label>Nama</Label>
<Controller
control={control}
name="name"
render={({ field }) => (
<Input {...field} placeholder="Masukkan nama" />
)}
/>
{errors.name && (
<p className="text-red-500 text-sm">{errors.name.message}</p>
)}
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 flex justify-end gap-3">
<Button type="submit">Update</Button>
<Button type="button" variant="outline" onClick={() => onCancel?.()}>
Cancel
</Button>
</div>
</Card>
</form>
);
}

View File

@ -68,7 +68,7 @@ export const useOTP = () => {
}, []); }, []);
const stopTimer = useCallback(() => { const stopTimer = useCallback(() => {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
isActive: false, isActive: false,
isExpired: true, isExpired: true,
@ -78,7 +78,7 @@ export const useOTP = () => {
// Timer effect // Timer effect
useEffect(() => { useEffect(() => {
if (!timer.isActive || timer.countdown <= 0) { if (!timer.isActive || timer.countdown <= 0) {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
isActive: false, isActive: false,
isExpired: true, isExpired: true,
@ -87,7 +87,7 @@ export const useOTP = () => {
} }
const interval = setInterval(() => { const interval = setInterval(() => {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
countdown: Math.max(0, prev.countdown - 1000), countdown: Math.max(0, prev.countdown - 1000),
})); }));
@ -96,103 +96,116 @@ export const useOTP = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [timer.isActive, timer.countdown]); }, [timer.isActive, timer.countdown]);
const requestOTPCode = useCallback(async ( const requestOTPCode = useCallback(
email: string, async (
category: UserCategory, email: string,
memberIdentity?: string category: UserCategory,
): Promise<boolean> => { memberIdentity?: string
try { ): Promise<boolean> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
// Check rate limiting // Check rate limiting
const identifier = `${email}-${category}`; const identifier = `${email}-${category}`;
if (!registrationRateLimiter.canAttempt(identifier)) { if (!registrationRateLimiter.canAttempt(identifier)) {
const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier); const remainingAttempts =
throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`); registrationRateLimiter.getRemainingAttempts(identifier);
throw new Error(
`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`
);
}
const data = {
memberIdentity: memberIdentity || null,
email,
category: getCategoryRoleId(category),
name: email.split("@")[0],
};
// Debug logging
console.log("OTP Request Data:", data);
console.log("Category before conversion:", category);
console.log("Category after conversion:", getCategoryRoleId(category));
const response = await requestOTP(data);
if (response?.error) {
registrationRateLimiter.recordAttempt(identifier);
throw new Error(response.message || "Failed to send OTP");
}
// Start timer on successful OTP request
startTimer();
showRegistrationInfo("OTP sent successfully. Please check your email.");
return true;
} catch (error: any) {
const errorMessage = error?.message || "Failed to send OTP";
setError(errorMessage);
showRegistrationError(error, "Failed to send OTP");
return false;
} finally {
setLoading(false);
} }
},
[startTimer]
);
const data = { const verifyOTPCode = useCallback(
memberIdentity: memberIdentity || null, async (
email, email: string,
category: getCategoryRoleId(category), otp: string,
}; category: UserCategory,
memberIdentity?: string
): Promise<any> => {
try {
setLoading(true);
setError(null);
// Debug logging if (otp.length !== 6) {
console.log("OTP Request Data:", data); throw new Error("OTP must be exactly 6 digits");
console.log("Category before conversion:", category); }
console.log("Category after conversion:", getCategoryRoleId(category));
const response = await requestOTP(data); const data = {
memberIdentity: memberIdentity || null,
email,
otp,
category: getCategoryRoleId(category),
};
if (response?.error) { const response = await verifyOTP(data.email, data.otp);
registrationRateLimiter.recordAttempt(identifier);
throw new Error(response.message || "Failed to send OTP"); if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
stopTimer();
showRegistrationSuccess("OTP verified successfully");
return response?.data?.userData;
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showRegistrationError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
} }
},
[stopTimer]
);
// Start timer on successful OTP request const resendOTP = useCallback(
startTimer(); async (
showRegistrationInfo("OTP sent successfully. Please check your email."); email: string,
category: UserCategory,
return true; memberIdentity?: string
} catch (error: any) { ): Promise<boolean> => {
const errorMessage = error?.message || "Failed to send OTP"; return await requestOTPCode(email, category, memberIdentity);
setError(errorMessage); },
showRegistrationError(error, "Failed to send OTP"); [requestOTPCode]
return false; );
} finally {
setLoading(false);
}
}, [startTimer]);
const verifyOTPCode = useCallback(async (
email: string,
otp: string,
category: UserCategory,
memberIdentity?: string
): Promise<any> => {
try {
setLoading(true);
setError(null);
if (otp.length !== 6) {
throw new Error("OTP must be exactly 6 digits");
}
const data = {
memberIdentity: memberIdentity || null,
email,
otp,
category: getCategoryRoleId(category),
};
const response = await verifyOTP(data.email, data.otp);
if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
stopTimer();
showRegistrationSuccess("OTP verified successfully");
return response?.data?.userData;
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showRegistrationError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
}
}, [stopTimer]);
const resendOTP = useCallback(async (
email: string,
category: UserCategory,
memberIdentity?: string
): Promise<boolean> => {
return await requestOTPCode(email, category, memberIdentity);
}, [requestOTPCode]);
return { return {
requestOTP: requestOTPCode, requestOTP: requestOTPCode,
@ -301,54 +314,60 @@ export const useInstituteData = (category?: number) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchInstitutes = useCallback(async (categoryId?: number) => { const fetchInstitutes = useCallback(
try { async (categoryId?: number) => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const response = await listInstitusi(categoryId || category); const response = await listInstitusi(categoryId || category);
if (response?.error) { if (response?.error) {
throw new Error(response.message || "Failed to fetch institutes"); throw new Error(response.message || "Failed to fetch institutes");
}
setInstitutes(response?.data?.data || []);
} catch (error: any) {
const errorMessage = error?.message || "Failed to fetch institutes";
setError(errorMessage);
showRegistrationError(error, "Failed to fetch institutes");
} finally {
setLoading(false);
} }
},
[category]
);
setInstitutes(response?.data?.data || []); const saveInstitute = useCallback(
} catch (error: any) { async (instituteData: InstituteData): Promise<number> => {
const errorMessage = error?.message || "Failed to fetch institutes"; try {
setError(errorMessage); setLoading(true);
showRegistrationError(error, "Failed to fetch institutes"); setError(null);
} finally {
setLoading(false);
}
}, [category]);
const saveInstitute = useCallback(async (instituteData: InstituteData): Promise<number> => { const sanitizedData = sanitizeInstituteData(instituteData);
try {
setLoading(true);
setError(null);
const sanitizedData = sanitizeInstituteData(instituteData); const response = await saveInstitutes({
name: sanitizedData.name,
address: sanitizedData.address,
categoryRoleId: category || 6, // Use provided category or default to Journalist category
});
const response = await saveInstitutes({ if (response?.error) {
name: sanitizedData.name, throw new Error(response.message || "Failed to save institute");
address: sanitizedData.address, }
categoryRoleId: category || 6, // Use provided category or default to Journalist category
});
if (response?.error) { return response?.data?.data?.id || 1;
throw new Error(response.message || "Failed to save institute"); } catch (error: any) {
const errorMessage = error?.message || "Failed to save institute";
setError(errorMessage);
showRegistrationError(error, "Failed to save institute");
throw error;
} finally {
setLoading(false);
} }
},
return response?.data?.data?.id || 1; [category]
} catch (error: any) { );
const errorMessage = error?.message || "Failed to save institute";
setError(errorMessage);
showRegistrationError(error, "Failed to save institute");
throw error;
} finally {
setLoading(false);
}
}, [category]);
// Load institutes on mount if category is provided // Load institutes on mount if category is provided
useEffect(() => { useEffect(() => {
@ -371,49 +390,59 @@ export const useUserDataValidation = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const validateJournalistData = useCallback(async (certificateNumber: string): Promise<any> => { const validateJournalistData = useCallback(
try { async (certificateNumber: string): Promise<any> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const response = await getDataJournalist(certificateNumber); const response = await getDataJournalist(certificateNumber);
if (response?.error) { if (response?.error) {
throw new Error(response.message || "Invalid journalist certificate number"); throw new Error(
response.message || "Invalid journalist certificate number"
);
}
return response?.data?.data;
} catch (error: any) {
const errorMessage =
error?.message || "Failed to validate journalist data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate journalist data");
throw error;
} finally {
setLoading(false);
} }
},
[]
);
return response?.data?.data; const validatePersonnelData = useCallback(
} catch (error: any) { async (policeNumber: string): Promise<any> => {
const errorMessage = error?.message || "Failed to validate journalist data"; try {
setError(errorMessage); setLoading(true);
showRegistrationError(error, "Failed to validate journalist data"); setError(null);
throw error;
} finally {
setLoading(false);
}
}, []);
const validatePersonnelData = useCallback(async (policeNumber: string): Promise<any> => { const response = await getDataPersonil(policeNumber);
try {
setLoading(true);
setError(null);
const response = await getDataPersonil(policeNumber); if (response?.error) {
throw new Error(response.message || "Invalid police number");
}
if (response?.error) { return response?.data?.data;
throw new Error(response.message || "Invalid police number"); } catch (error: any) {
const errorMessage =
error?.message || "Failed to validate personnel data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate personnel data");
throw error;
} finally {
setLoading(false);
} }
},
return response?.data?.data; []
} catch (error: any) { );
const errorMessage = error?.message || "Failed to validate personnel data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate personnel data");
throw error;
} finally {
setLoading(false);
}
}, []);
return { return {
validateJournalistData, validateJournalistData,
@ -429,57 +458,70 @@ export const useRegistration = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const submitRegistration = useCallback(async ( const submitRegistration = useCallback(
data: RegistrationFormData, async (
category: UserCategory, data: RegistrationFormData,
userData: any, category: UserCategory,
instituteId?: number userData: any,
): Promise<boolean> => { instituteId?: number
try { ): Promise<boolean> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
// Sanitize and validate data // Sanitize and validate data
const sanitizedData = sanitizeRegistrationData(data); const sanitizedData = sanitizeRegistrationData(data);
// Validate password // Validate password
const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf); const passwordValidation = validatePassword(
if (!passwordValidation.isValid) { sanitizedData.password,
throw new Error(passwordValidation.errors[0]); sanitizedData.passwordConf
);
if (!passwordValidation.isValid) {
throw new Error(passwordValidation.errors[0]);
}
// Validate username
const usernameValidation = validateUsername(sanitizedData.username);
if (!usernameValidation.isValid) {
throw new Error(usernameValidation.error!);
}
// Transform data for API
const transformedData = transformRegistrationData(
sanitizedData,
category,
userData,
instituteId
);
const response = await postRegistration(transformedData);
console.log("PPPP", transformedData);
if (response?.error) {
throw new Error(response.message || "Registration failed");
}
showRegistrationSuccess(
"Registration successful! Please check your email for verification."
);
// Redirect to login page
setTimeout(() => {
router.push("/auth");
}, 2000);
return true;
} catch (error: any) {
const errorMessage = error?.message || "Registration failed";
setError(errorMessage);
showRegistrationError(error, "Registration failed");
return false;
} finally {
setLoading(false);
} }
},
// Validate username [router]
const usernameValidation = validateUsername(sanitizedData.username); );
if (!usernameValidation.isValid) {
throw new Error(usernameValidation.error!);
}
// Transform data for API
const transformedData = transformRegistrationData(sanitizedData, category, userData, instituteId);
const response = await postRegistration(transformedData);
console.log("PPPP", transformedData)
if (response?.error) {
throw new Error(response.message || "Registration failed");
}
showRegistrationSuccess("Registration successful! Please check your email for verification.");
// Redirect to login page
setTimeout(() => {
router.push("/auth");
}, 2000);
return true;
} catch (error: any) {
const errorMessage = error?.message || "Registration failed";
setError(errorMessage);
showRegistrationError(error, "Registration failed");
return false;
} finally {
setLoading(false);
}
}, [router]);
return { return {
submitRegistration, submitRegistration,
@ -490,78 +532,90 @@ export const useRegistration = () => {
// Hook for form validation // Hook for form validation
export const useFormValidation = () => { export const useFormValidation = () => {
const validateIdentityForm = useCallback(( const validateIdentityForm = useCallback(
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData, (
category: UserCategory data:
): { isValid: boolean; errors: string[] } => { | JournalistRegistrationData
return validateIdentityData(data, category); | PersonnelRegistrationData
}, []); | GeneralRegistrationData,
category: UserCategory
): { isValid: boolean; errors: string[] } => {
return validateIdentityData(data, category);
},
[]
);
const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => { const validateProfileForm = useCallback(
const errors: string[] = []; (data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
// Validate required fields // Validate required fields
if (!data.firstName?.trim()) { if (!data.firstName?.trim()) {
errors.push("Full name is required"); errors.push("Full name is required");
}
if (!data.username?.trim()) {
errors.push("Username is required");
} else {
const usernameValidation = validateUsername(data.username);
if (!usernameValidation.isValid) {
errors.push(usernameValidation.error!);
} }
}
if (!data.email?.trim()) { if (!data.username?.trim()) {
errors.push("Email is required"); errors.push("Username is required");
} else { } else {
const emailValidation = validateEmail(data.email); const usernameValidation = validateUsername(data.username);
if (!emailValidation.isValid) { if (!usernameValidation.isValid) {
errors.push(emailValidation.error!); errors.push(usernameValidation.error!);
}
} }
}
if (!data.phoneNumber?.trim()) { if (!data.email?.trim()) {
errors.push("Phone number is required"); errors.push("Email is required");
} else { } else {
const phoneValidation = validatePhoneNumber(data.phoneNumber); const emailValidation = validateEmail(data.email);
if (!phoneValidation.isValid) { if (!emailValidation.isValid) {
errors.push(phoneValidation.error!); errors.push(emailValidation.error!);
}
} }
}
if (!data.address?.trim()) { if (!data.phoneNumber?.trim()) {
errors.push("Address is required"); errors.push("Phone number is required");
} } else {
const phoneValidation = validatePhoneNumber(data.phoneNumber);
if (!data.provinsi) { if (!phoneValidation.isValid) {
errors.push("Province is required"); errors.push(phoneValidation.error!);
} }
if (!data.kota) {
errors.push("City is required");
}
if (!data.kecamatan) {
errors.push("Subdistrict is required");
}
if (!data.password) {
errors.push("Password is required");
} else {
const passwordValidation = validatePassword(data.password, data.passwordConf);
if (!passwordValidation.isValid) {
errors.push(passwordValidation.errors[0]);
} }
}
return { if (!data.address?.trim()) {
isValid: errors.length === 0, errors.push("Address is required");
errors, }
};
}, []); if (!data.provinsi) {
errors.push("Province is required");
}
if (!data.kota) {
errors.push("City is required");
}
if (!data.kecamatan) {
errors.push("Subdistrict is required");
}
if (!data.password) {
errors.push("Password is required");
} else {
const passwordValidation = validatePassword(
data.password,
data.passwordConf
);
if (!passwordValidation.isValid) {
errors.push(passwordValidation.errors[0]);
}
}
return {
isValid: errors.length === 0,
errors,
};
},
[]
);
return { return {
validateIdentityForm, validateIdentityForm,

View File

@ -68,7 +68,7 @@ export const useOTP = () => {
}, []); }, []);
const stopTimer = useCallback(() => { const stopTimer = useCallback(() => {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
isActive: false, isActive: false,
isExpired: true, isExpired: true,
@ -78,7 +78,7 @@ export const useOTP = () => {
// Timer effect // Timer effect
useEffect(() => { useEffect(() => {
if (!timer.isActive || timer.countdown <= 0) { if (!timer.isActive || timer.countdown <= 0) {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
isActive: false, isActive: false,
isExpired: true, isExpired: true,
@ -87,7 +87,7 @@ export const useOTP = () => {
} }
const interval = setInterval(() => { const interval = setInterval(() => {
setTimer(prev => ({ setTimer((prev) => ({
...prev, ...prev,
countdown: Math.max(0, prev.countdown - 1000), countdown: Math.max(0, prev.countdown - 1000),
})); }));
@ -96,103 +96,116 @@ export const useOTP = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [timer.isActive, timer.countdown]); }, [timer.isActive, timer.countdown]);
const requestOTPCode = useCallback(async ( const requestOTPCode = useCallback(
email: string, async (
category: UserCategory, email: string,
memberIdentity?: string category: UserCategory,
): Promise<boolean> => { memberIdentity?: string
try { ): Promise<boolean> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
// Check rate limiting // Check rate limiting
const identifier = `${email}-${category}`; const identifier = `${email}-${category}`;
if (!registrationRateLimiter.canAttempt(identifier)) { if (!registrationRateLimiter.canAttempt(identifier)) {
const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier); const remainingAttempts =
throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`); registrationRateLimiter.getRemainingAttempts(identifier);
throw new Error(
`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`
);
}
const data = {
memberIdentity: memberIdentity || null,
email,
category: getCategoryRoleId(category),
name: email.split("@")[0],
};
// Debug logging
console.log("OTP Request Data:", data);
console.log("Category before conversion:", category);
console.log("Category after conversion:", getCategoryRoleId(category));
const response = await requestOTP(data);
if (response?.error) {
registrationRateLimiter.recordAttempt(identifier);
throw new Error(response.message || "Failed to send OTP");
}
// Start timer on successful OTP request
startTimer();
showRegistrationInfo("OTP sent successfully. Please check your email.");
return true;
} catch (error: any) {
const errorMessage = error?.message || "Failed to send OTP";
setError(errorMessage);
showRegistrationError(error, "Failed to send OTP");
return false;
} finally {
setLoading(false);
} }
},
[startTimer]
);
const data = { const verifyOTPCode = useCallback(
memberIdentity: memberIdentity || null, async (
email, email: string,
category: getCategoryRoleId(category), otp: string,
}; category: UserCategory,
memberIdentity?: string
): Promise<any> => {
try {
setLoading(true);
setError(null);
// Debug logging if (otp.length !== 6) {
console.log("OTP Request Data:", data); throw new Error("OTP must be exactly 6 digits");
console.log("Category before conversion:", category); }
console.log("Category after conversion:", getCategoryRoleId(category));
const response = await requestOTP(data); const data = {
memberIdentity: memberIdentity || null,
email,
otp,
category: getCategoryRoleId(category),
};
if (response?.error) { const response = await verifyOTP(data.email, data.otp);
registrationRateLimiter.recordAttempt(identifier);
throw new Error(response.message || "Failed to send OTP"); if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
stopTimer();
showRegistrationSuccess("OTP verified successfully");
return response?.data?.userData;
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showRegistrationError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
} }
},
[stopTimer]
);
// Start timer on successful OTP request const resendOTP = useCallback(
startTimer(); async (
showRegistrationInfo("OTP sent successfully. Please check your email."); email: string,
category: UserCategory,
return true; memberIdentity?: string
} catch (error: any) { ): Promise<boolean> => {
const errorMessage = error?.message || "Failed to send OTP"; return await requestOTPCode(email, category, memberIdentity);
setError(errorMessage); },
showRegistrationError(error, "Failed to send OTP"); [requestOTPCode]
return false; );
} finally {
setLoading(false);
}
}, [startTimer]);
const verifyOTPCode = useCallback(async (
email: string,
otp: string,
category: UserCategory,
memberIdentity?: string
): Promise<any> => {
try {
setLoading(true);
setError(null);
if (otp.length !== 6) {
throw new Error("OTP must be exactly 6 digits");
}
const data = {
memberIdentity: memberIdentity || null,
email,
otp,
category: getCategoryRoleId(category),
};
const response = await verifyOTP(data.email, data.otp);
if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
stopTimer();
showRegistrationSuccess("OTP verified successfully");
return response?.data?.userData;
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showRegistrationError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
}
}, [stopTimer]);
const resendOTP = useCallback(async (
email: string,
category: UserCategory,
memberIdentity?: string
): Promise<boolean> => {
return await requestOTPCode(email, category, memberIdentity);
}, [requestOTPCode]);
return { return {
requestOTP: requestOTPCode, requestOTP: requestOTPCode,
@ -301,54 +314,60 @@ export const useInstituteData = (category?: number) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchInstitutes = useCallback(async (categoryId?: number) => { const fetchInstitutes = useCallback(
try { async (categoryId?: number) => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const response = await listInstitusi(categoryId || category); const response = await listInstitusi(categoryId || category);
if (response?.error) { if (response?.error) {
throw new Error(response.message || "Failed to fetch institutes"); throw new Error(response.message || "Failed to fetch institutes");
}
setInstitutes(response?.data?.data || []);
} catch (error: any) {
const errorMessage = error?.message || "Failed to fetch institutes";
setError(errorMessage);
showRegistrationError(error, "Failed to fetch institutes");
} finally {
setLoading(false);
} }
},
[category]
);
setInstitutes(response?.data?.data || []); const saveInstitute = useCallback(
} catch (error: any) { async (instituteData: InstituteData): Promise<number> => {
const errorMessage = error?.message || "Failed to fetch institutes"; try {
setError(errorMessage); setLoading(true);
showRegistrationError(error, "Failed to fetch institutes"); setError(null);
} finally {
setLoading(false);
}
}, [category]);
const saveInstitute = useCallback(async (instituteData: InstituteData): Promise<number> => { const sanitizedData = sanitizeInstituteData(instituteData);
try {
setLoading(true);
setError(null);
const sanitizedData = sanitizeInstituteData(instituteData); const response = await saveInstitutes({
name: sanitizedData.name,
address: sanitizedData.address,
categoryRoleId: category || 6, // Use provided category or default to Journalist category
});
const response = await saveInstitutes({ if (response?.error) {
name: sanitizedData.name, throw new Error(response.message || "Failed to save institute");
address: sanitizedData.address, }
categoryRoleId: category || 6, // Use provided category or default to Journalist category
});
if (response?.error) { return response?.data?.data?.id || 1;
throw new Error(response.message || "Failed to save institute"); } catch (error: any) {
const errorMessage = error?.message || "Failed to save institute";
setError(errorMessage);
showRegistrationError(error, "Failed to save institute");
throw error;
} finally {
setLoading(false);
} }
},
return response?.data?.data?.id || 1; [category]
} catch (error: any) { );
const errorMessage = error?.message || "Failed to save institute";
setError(errorMessage);
showRegistrationError(error, "Failed to save institute");
throw error;
} finally {
setLoading(false);
}
}, [category]);
// Load institutes on mount if category is provided // Load institutes on mount if category is provided
useEffect(() => { useEffect(() => {
@ -371,49 +390,59 @@ export const useUserDataValidation = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const validateJournalistData = useCallback(async (certificateNumber: string): Promise<any> => { const validateJournalistData = useCallback(
try { async (certificateNumber: string): Promise<any> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const response = await getDataJournalist(certificateNumber); const response = await getDataJournalist(certificateNumber);
if (response?.error) { if (response?.error) {
throw new Error(response.message || "Invalid journalist certificate number"); throw new Error(
response.message || "Invalid journalist certificate number"
);
}
return response?.data?.data;
} catch (error: any) {
const errorMessage =
error?.message || "Failed to validate journalist data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate journalist data");
throw error;
} finally {
setLoading(false);
} }
},
[]
);
return response?.data?.data; const validatePersonnelData = useCallback(
} catch (error: any) { async (policeNumber: string): Promise<any> => {
const errorMessage = error?.message || "Failed to validate journalist data"; try {
setError(errorMessage); setLoading(true);
showRegistrationError(error, "Failed to validate journalist data"); setError(null);
throw error;
} finally {
setLoading(false);
}
}, []);
const validatePersonnelData = useCallback(async (policeNumber: string): Promise<any> => { const response = await getDataPersonil(policeNumber);
try {
setLoading(true);
setError(null);
const response = await getDataPersonil(policeNumber); if (response?.error) {
throw new Error(response.message || "Invalid police number");
}
if (response?.error) { return response?.data?.data;
throw new Error(response.message || "Invalid police number"); } catch (error: any) {
const errorMessage =
error?.message || "Failed to validate personnel data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate personnel data");
throw error;
} finally {
setLoading(false);
} }
},
return response?.data?.data; []
} catch (error: any) { );
const errorMessage = error?.message || "Failed to validate personnel data";
setError(errorMessage);
showRegistrationError(error, "Failed to validate personnel data");
throw error;
} finally {
setLoading(false);
}
}, []);
return { return {
validateJournalistData, validateJournalistData,
@ -429,57 +458,70 @@ export const useRegistration = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const submitRegistration = useCallback(async ( const submitRegistration = useCallback(
data: RegistrationFormData, async (
category: UserCategory, data: RegistrationFormData,
userData: any, category: UserCategory,
instituteId?: number userData: any,
): Promise<boolean> => { instituteId?: number
try { ): Promise<boolean> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
// Sanitize and validate data // Sanitize and validate data
const sanitizedData = sanitizeRegistrationData(data); const sanitizedData = sanitizeRegistrationData(data);
// Validate password // Validate password
const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf); const passwordValidation = validatePassword(
if (!passwordValidation.isValid) { sanitizedData.password,
throw new Error(passwordValidation.errors[0]); sanitizedData.passwordConf
);
if (!passwordValidation.isValid) {
throw new Error(passwordValidation.errors[0]);
}
// Validate username
const usernameValidation = validateUsername(sanitizedData.username);
if (!usernameValidation.isValid) {
throw new Error(usernameValidation.error!);
}
// Transform data for API
const transformedData = transformRegistrationData(
sanitizedData,
category,
userData,
instituteId
);
const response = await postRegistration(transformedData);
console.log("PPPP", transformedData);
if (response?.error) {
throw new Error(response.message || "Registration failed");
}
showRegistrationSuccess(
"Registration successful! Please check your email for verification."
);
// Redirect to login page
setTimeout(() => {
router.push("/auth");
}, 2000);
return true;
} catch (error: any) {
const errorMessage = error?.message || "Registration failed";
setError(errorMessage);
showRegistrationError(error, "Registration failed");
return false;
} finally {
setLoading(false);
} }
},
// Validate username [router]
const usernameValidation = validateUsername(sanitizedData.username); );
if (!usernameValidation.isValid) {
throw new Error(usernameValidation.error!);
}
// Transform data for API
const transformedData = transformRegistrationData(sanitizedData, category, userData, instituteId);
const response = await postRegistration(transformedData);
console.log("PPPP", transformedData)
if (response?.error) {
throw new Error(response.message || "Registration failed");
}
showRegistrationSuccess("Registration successful! Please check your email for verification.");
// Redirect to login page
setTimeout(() => {
router.push("/auth");
}, 2000);
return true;
} catch (error: any) {
const errorMessage = error?.message || "Registration failed";
setError(errorMessage);
showRegistrationError(error, "Registration failed");
return false;
} finally {
setLoading(false);
}
}, [router]);
return { return {
submitRegistration, submitRegistration,
@ -490,78 +532,90 @@ export const useRegistration = () => {
// Hook for form validation // Hook for form validation
export const useFormValidation = () => { export const useFormValidation = () => {
const validateIdentityForm = useCallback(( const validateIdentityForm = useCallback(
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData, (
category: UserCategory data:
): { isValid: boolean; errors: string[] } => { | JournalistRegistrationData
return validateIdentityData(data, category); | PersonnelRegistrationData
}, []); | GeneralRegistrationData,
category: UserCategory
): { isValid: boolean; errors: string[] } => {
return validateIdentityData(data, category);
},
[]
);
const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => { const validateProfileForm = useCallback(
const errors: string[] = []; (data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
// Validate required fields // Validate required fields
if (!data.firstName?.trim()) { if (!data.firstName?.trim()) {
errors.push("Full name is required"); errors.push("Full name is required");
}
if (!data.username?.trim()) {
errors.push("Username is required");
} else {
const usernameValidation = validateUsername(data.username);
if (!usernameValidation.isValid) {
errors.push(usernameValidation.error!);
} }
}
if (!data.email?.trim()) { if (!data.username?.trim()) {
errors.push("Email is required"); errors.push("Username is required");
} else { } else {
const emailValidation = validateEmail(data.email); const usernameValidation = validateUsername(data.username);
if (!emailValidation.isValid) { if (!usernameValidation.isValid) {
errors.push(emailValidation.error!); errors.push(usernameValidation.error!);
}
} }
}
if (!data.phoneNumber?.trim()) { if (!data.email?.trim()) {
errors.push("Phone number is required"); errors.push("Email is required");
} else { } else {
const phoneValidation = validatePhoneNumber(data.phoneNumber); const emailValidation = validateEmail(data.email);
if (!phoneValidation.isValid) { if (!emailValidation.isValid) {
errors.push(phoneValidation.error!); errors.push(emailValidation.error!);
}
} }
}
if (!data.address?.trim()) { if (!data.phoneNumber?.trim()) {
errors.push("Address is required"); errors.push("Phone number is required");
} } else {
const phoneValidation = validatePhoneNumber(data.phoneNumber);
if (!data.provinsi) { if (!phoneValidation.isValid) {
errors.push("Province is required"); errors.push(phoneValidation.error!);
} }
if (!data.kota) {
errors.push("City is required");
}
if (!data.kecamatan) {
errors.push("Subdistrict is required");
}
if (!data.password) {
errors.push("Password is required");
} else {
const passwordValidation = validatePassword(data.password, data.passwordConf);
if (!passwordValidation.isValid) {
errors.push(passwordValidation.errors[0]);
} }
}
return { if (!data.address?.trim()) {
isValid: errors.length === 0, errors.push("Address is required");
errors, }
};
}, []); if (!data.provinsi) {
errors.push("Province is required");
}
if (!data.kota) {
errors.push("City is required");
}
if (!data.kecamatan) {
errors.push("Subdistrict is required");
}
if (!data.password) {
errors.push("Password is required");
} else {
const passwordValidation = validatePassword(
data.password,
data.passwordConf
);
if (!passwordValidation.isValid) {
errors.push(passwordValidation.errors[0]);
}
}
return {
isValid: errors.length === 0,
errors,
};
},
[]
);
return { return {
validateIdentityForm, validateIdentityForm,

View File

@ -90,3 +90,41 @@ export function successToast(title: string, text: string) {
text: text, text: text,
}); });
} }
// ✅ Notifikasi sukses auto-close
export function successAutoClose(message: string, duration = 3000) {
Swal.fire({
title: "Sukses!",
text: message,
icon: "success",
timer: duration,
showConfirmButton: false,
timerProgressBar: true,
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
const popup = Swal.getPopup();
popup?.addEventListener("mouseenter", Swal.stopTimer);
popup?.addEventListener("mouseleave", Swal.resumeTimer);
},
});
}
// ❌ Notifikasi error auto-close
export function errorAutoClose(message: string, duration = 3000) {
Swal.fire({
title: "Gagal!",
text: message,
icon: "error",
timer: duration,
showConfirmButton: false,
timerProgressBar: true,
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
const popup = Swal.getPopup();
popup?.addEventListener("mouseenter", Swal.stopTimer);
popup?.addEventListener("mouseleave", Swal.resumeTimer);
},
});
}

View File

@ -4,6 +4,7 @@ import {
httpDeleteInterceptor, httpDeleteInterceptor,
httpGetInterceptor, httpGetInterceptor,
httpPostInterceptor, httpPostInterceptor,
httpPutInterceptor,
} from "../http-config/http-interceptor-service"; } from "../http-config/http-interceptor-service";
interface GetCategoriesParams { interface GetCategoriesParams {
@ -80,8 +81,20 @@ export async function createCategories(data: CreateCategoryPayload) {
return httpPostInterceptor(url, data); return httpPostInterceptor(url, data);
} }
export async function getArticleCategoryDetail(id: number) { export async function getArticleCategoryDetail(id: number) {
const url = `article-categories/${id}`; const url = `article-categories/${id}`;
return await httpGetInterceptor(url); return await httpGetInterceptor(url);
} }
export async function updateArticleCategory(id: number, data: any) {
const url = `article-categories/${id}`;
return httpPutInterceptor(url, data);
}
export async function uploadArticleCategoryThumbnail(id: any, data: any) {
const url = `article-categories/thumbnail/${id}`;
const headers = {
"Content-Type": "multipart/form-data",
};
return httpPostInterceptor(url, data, headers);
}

View File

@ -314,6 +314,10 @@ export async function deleteMedia(data: any) {
return httpDeleteInterceptor(url, data); return httpDeleteInterceptor(url, data);
} }
export async function deleteArticle(id: number) {
const url = `articles/${id}`;
return httpDeleteInterceptor(url);
}
export async function deleteFile(data: any) { export async function deleteFile(data: any) {
const url = "media/file"; const url = "media/file";
return httpDeleteInterceptor(url, data); return httpDeleteInterceptor(url, data);

View File

@ -1,6 +1,18 @@
import { httpDeleteInterceptor } from "./http-config/http-interceptor-service"; import { httpDeleteInterceptor, httpGetInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-service";
export async function deleteUserLevel(id: number) { export async function deleteUserLevel(id: number) {
const url = `user-levels/${id}`; const url = `user-levels/${id}`;
return await httpDeleteInterceptor(url); return await httpDeleteInterceptor(url);
} }
export async function updateUserLevel(id: number, data: any) {
const url = `user-levels/${id}`;
return httpPutInterceptor(url, data);
}
export async function getUserLevelDetail(id: number) {
const url = `user-levels/${id}`;
return httpGetInterceptor(url);
}