fix: all section in tenant and others
This commit is contained in:
parent
f44a7c4be1
commit
e2d0e17846
|
|
@ -1,11 +1,12 @@
|
|||
import CategoriesDetailForm from "@/components/form/categories/categories-detail-form";
|
||||
import FormImageDetail from "@/components/form/content/image/image-detail-form";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
|
||||
const CategoriesDetailPage = async () => {
|
||||
return (
|
||||
<div>
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="space-y-4">
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<CategoriesDetailForm />{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -17,6 +17,7 @@ import withReactContent from "sweetalert2-react-content";
|
|||
import { error } from "@/lib/swal";
|
||||
import Link from "next/link";
|
||||
import { deleteMedia } from "@/service/content";
|
||||
import { deleteArticle } from "@/service/content/content";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -55,12 +56,10 @@ const useTableColumns = () => {
|
|||
const categoryName = row.getValue("categoryName");
|
||||
const categories = row.original.categories;
|
||||
// Handle new API structure with categories array
|
||||
const displayName = categoryName || (categories && categories.length > 0 ? categories[0].title : "-");
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
const displayName =
|
||||
categoryName ||
|
||||
(categories && categories.length > 0 ? categories[0].title : "-");
|
||||
return <span className="whitespace-nowrap">{displayName}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -191,13 +190,8 @@ const useTableColumns = () => {
|
|||
const MySwal = withReactContent(Swal);
|
||||
|
||||
async function doDelete(id: any) {
|
||||
// loading();
|
||||
const data = {
|
||||
id,
|
||||
};
|
||||
|
||||
const response = await deleteMedia(data);
|
||||
|
||||
const data = { id };
|
||||
const response = await deleteArticle(id);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
|
|
@ -275,8 +269,8 @@ const useTableColumns = () => {
|
|||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link> */}
|
||||
{(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && (
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
<Link
|
||||
href={`/admin/content/audio-visual/update/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -285,7 +279,7 @@ const useTableColumns = () => {
|
|||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
{/* )} */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
|
||||
|
|
|
|||
|
|
@ -176,13 +176,12 @@ const TableVideo = () => {
|
|||
: "";
|
||||
|
||||
try {
|
||||
// Using the new interface-based approach for video content
|
||||
const filters: ArticleFilters = {
|
||||
page: page,
|
||||
page,
|
||||
totalPage: Number(showData),
|
||||
title: search || undefined,
|
||||
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
|
||||
typeId: 2, // video content type
|
||||
typeId: 2, // ✅ untuk video
|
||||
statusId:
|
||||
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
|
||||
startDate: formattedStartDate || undefined,
|
||||
|
|
@ -197,28 +196,28 @@ const TableVideo = () => {
|
|||
item.no = (page - 1) * Number(showData) + index + 1;
|
||||
});
|
||||
setDataTable(data);
|
||||
setTotalData(data.length);
|
||||
setTotalPage(Math.ceil(data.length / Number(showData)));
|
||||
setTotalData(res?.data?.meta?.count || data.length);
|
||||
setTotalPage(
|
||||
Math.ceil((res?.data?.meta?.count || data.length) / Number(showData))
|
||||
);
|
||||
} else if (Array.isArray(data?.content)) {
|
||||
const contentData = data.content;
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * Number(showData) + index + 1;
|
||||
});
|
||||
setDataTable(contentData);
|
||||
setTotalData(data?.totalElements ?? contentData.length);
|
||||
setTotalData(res?.data?.meta?.count || contentData.length);
|
||||
setTotalPage(
|
||||
data?.totalPages ?? Math.ceil(contentData.length / Number(showData))
|
||||
Math.ceil(
|
||||
(res?.data?.meta?.count || contentData.length) / Number(showData)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setDataTable([]);
|
||||
setTotalData(0);
|
||||
setTotalPage(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching tasks:", error);
|
||||
} catch (err) {
|
||||
console.error("Error fetching tasks:", err);
|
||||
setDataTable([]);
|
||||
setTotalData(0);
|
||||
setTotalPage(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import SiteBreadcrumb from "@/components/site-breadcrumb";
|
|||
const VideoCreatePage = async () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4 m-3">
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormVideo />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { format } from "date-fns";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { deleteMedia } from "@/service/content/content";
|
||||
import { error } from "@/lib/swal";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Link from "next/link";
|
||||
import { deleteArticle } from "@/service/content/content";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -172,7 +172,7 @@ const useTableColumns = () => {
|
|||
|
||||
async function doDelete(id: any) {
|
||||
const data = { id };
|
||||
const response = await deleteMedia(data);
|
||||
const response = await deleteArticle(id);
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
|
|
@ -240,14 +240,14 @@ const useTableColumns = () => {
|
|||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && (
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && ( */}
|
||||
<Link href={`/admin/content/image/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
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|||
import TablePagination from "@/components/table/table-pagination";
|
||||
|
||||
import {
|
||||
deleteMedia,
|
||||
listDataImage,
|
||||
listArticles,
|
||||
listArticlesWithFilters,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import SiteBreadcrumb from "@/components/site-breadcrumb";
|
|||
const ImageCreatePage = async () => {
|
||||
return (
|
||||
<div>
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="space-y-4">
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormImage />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import FormImageDetail from "@/components/form/content/image/image-detail-form";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
|
||||
const ImageDetailPage = async () => {
|
||||
return (
|
||||
<div>
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="space-y-4">
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormImageDetail />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import FormImageUpdate from "@/components/form/content/image/image-update-form";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
|
||||
const ImageUpdatePage = async () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4">
|
||||
<FormImageUpdate />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,17 +11,21 @@ import {
|
|||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { format } from "date-fns";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { deleteMedia } from "@/service/content/content";
|
||||
import { error } from "@/lib/swal";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Link from "next/link";
|
||||
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 userLevelId = getCookiesDecrypt("ulie");
|
||||
|
||||
|
|
@ -185,6 +189,23 @@ const useTableColumns = () => {
|
|||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
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) {
|
||||
const response = await deleteUserLevel(id);
|
||||
|
|
@ -242,7 +263,6 @@ const useTableColumns = () => {
|
|||
const userId = getCookiesDecrypt("uie");
|
||||
const userLevelId = getCookiesDecrypt("ulie");
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
|
|
@ -251,43 +271,91 @@ const useTableColumns = () => {
|
|||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
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" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ import {
|
|||
} from "@tanstack/react-table";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
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() {
|
||||
const [activeTab, setActiveTab] = useState("workflows");
|
||||
|
|
@ -59,6 +62,14 @@ function TenantSettingsContentTable() {
|
|||
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
|
||||
const { checkWorkflowStatus } = useWorkflowStatusCheck();
|
||||
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(() => {
|
||||
loadData();
|
||||
|
|
@ -90,6 +101,8 @@ function TenantSettingsContentTable() {
|
|||
});
|
||||
setUserLevels(data);
|
||||
console.log("LLL", data);
|
||||
setTotalData(data.length);
|
||||
setTotalPage(Math.ceil(data.length / Number(showData)));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
|
|
@ -107,19 +120,29 @@ function TenantSettingsContentTable() {
|
|||
|
||||
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
||||
try {
|
||||
loading();
|
||||
|
||||
const response = await createUserLevel(data);
|
||||
|
||||
close();
|
||||
|
||||
if (response?.error) {
|
||||
console.error("Error creating user level:", response?.message);
|
||||
} else {
|
||||
console.log("User level created successfully:", response);
|
||||
errorAutoClose(response.message || "Gagal membuat user level.");
|
||||
return;
|
||||
}
|
||||
|
||||
successAutoClose("User level berhasil dibuat.");
|
||||
|
||||
setIsUserLevelDialogOpen(false);
|
||||
|
||||
setTimeout(async () => {
|
||||
await loadData();
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
close();
|
||||
errorAutoClose("Terjadi kesalahan saat membuat user level.");
|
||||
console.error("Error creating user level:", error);
|
||||
}
|
||||
|
||||
setIsUserLevelDialogOpen(false);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => {
|
||||
|
|
@ -127,7 +150,10 @@ function TenantSettingsContentTable() {
|
|||
await loadData();
|
||||
};
|
||||
|
||||
const columns = useTableColumns();
|
||||
const columns = React.useMemo(
|
||||
() => useTableColumns((data) => handleEditUserLevel(data)),
|
||||
[]
|
||||
);
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
|
|
@ -759,7 +785,7 @@ function TenantSettingsContentTable() {
|
|||
)}
|
||||
|
||||
<Table className="overflow-hidden mt-3 mx-3">
|
||||
<TableHeader>
|
||||
<TableHeader className="sticky top-0 bg-white shadow-sm z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
|
|
@ -805,6 +831,45 @@ function TenantSettingsContentTable() {
|
|||
)}
|
||||
</TableBody>
|
||||
</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
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>page</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
|
|
@ -44,28 +44,21 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
isActive: true,
|
||||
});
|
||||
|
||||
// Bulk form state
|
||||
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
|
||||
|
||||
// API data
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [provinces, setProvinces] = useState<Province[]>([]);
|
||||
|
||||
// UI state
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(mode === "single" ? "basic" : "bulk");
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setFormData(initialData);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// Load API data
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
|
|
@ -86,7 +79,6 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
loadData();
|
||||
}, []);
|
||||
|
||||
// Validation
|
||||
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
|
|
@ -133,7 +125,6 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
return isValid;
|
||||
};
|
||||
|
||||
// Form handlers
|
||||
const handleFieldChange = (field: keyof UserLevelsCreateRequest, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
|
|
@ -151,11 +142,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
// Update mode based on active tab
|
||||
if (value === "bulk") {
|
||||
// Mode will be determined by activeTab in handleSubmit
|
||||
} else {
|
||||
// Mode will be determined by activeTab in handleSubmit
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export default function CategoriesDetailForm() {
|
|||
return (
|
||||
<form>
|
||||
{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 */}
|
||||
<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>
|
||||
|
||||
{/* Title */}
|
||||
|
|
@ -79,7 +79,7 @@ export default function CategoriesDetailForm() {
|
|||
{/* Thumbnail */}
|
||||
<div className="space-y-2 py-3">
|
||||
<Label>Thumbnail</Label>
|
||||
<Card className="mt-2 w-fit">
|
||||
<Card className="mt-2 w-fit p-2 border">
|
||||
<img
|
||||
src={detail.thumbnailUrl}
|
||||
alt="Category Thumbnail"
|
||||
|
|
@ -89,9 +89,9 @@ export default function CategoriesDetailForm() {
|
|||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="space-y-2 py-3">
|
||||
<div className="space-y-2 py-3 ">
|
||||
<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.map((tag: string, i: number) => (
|
||||
<Badge key={i} className="px-2 py-1 rounded-md">
|
||||
|
|
@ -106,7 +106,7 @@ export default function CategoriesDetailForm() {
|
|||
</Card>
|
||||
|
||||
{/* 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">
|
||||
{/* Creator */}
|
||||
<div>
|
||||
|
|
@ -155,7 +155,7 @@ export default function CategoriesDetailForm() {
|
|||
<Button
|
||||
type="button"
|
||||
onClick={() => router.push("/admin/categories")}
|
||||
className="mt-4"
|
||||
className="mt-4 border"
|
||||
>
|
||||
Back to Categories
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -329,26 +329,30 @@ export default function FormImageDetail() {
|
|||
const mappedDetail: Detail = {
|
||||
...details,
|
||||
// Map legacy fields for backward compatibility
|
||||
category: details.categories && details.categories.length > 0 ? {
|
||||
id: details.categories[0].id,
|
||||
name: details.categories[0].title
|
||||
} : undefined,
|
||||
category:
|
||||
details.categories && details.categories.length > 0
|
||||
? {
|
||||
id: details.categories[0].id,
|
||||
name: details.categories[0].title,
|
||||
}
|
||||
: undefined,
|
||||
creatorName: details.createdByName,
|
||||
thumbnailLink: details.thumbnailUrl,
|
||||
statusName: getStatusName(details.statusId),
|
||||
needApprovalFromLevel: 0, // This might need to be updated based on your business logic
|
||||
uploadedById: details.createdById,
|
||||
files: details.files || []
|
||||
files: details.files || [],
|
||||
};
|
||||
|
||||
// Map files from new API structure to expected format
|
||||
const mappedFiles = (mappedDetail.files || []).map((file: any) => ({
|
||||
id: file.id,
|
||||
url: file.fileUrl || file.url,
|
||||
thumbnailFileUrl: file.fileThumbnail || file.thumbnailFileUrl || file.fileUrl,
|
||||
thumbnailFileUrl:
|
||||
file.fileThumbnail || file.thumbnailFileUrl || file.fileUrl,
|
||||
fileName: file.fileName || file.fileName,
|
||||
// Keep original API fields for reference
|
||||
...file
|
||||
...file,
|
||||
}));
|
||||
|
||||
setFiles(mappedFiles);
|
||||
|
|
@ -367,8 +371,12 @@ export default function FormImageDetail() {
|
|||
// Set the selected target to the category ID from details
|
||||
setSelectedTarget(String(mappedDetail.categoryId));
|
||||
|
||||
const fileUrls = mappedFiles.map((file: any) =>
|
||||
file.thumbnailFileUrl || file.url || mappedDetail.thumbnailUrl || "default-image.jpg"
|
||||
const fileUrls = mappedFiles.map(
|
||||
(file: any) =>
|
||||
file.thumbnailFileUrl ||
|
||||
file.url ||
|
||||
mappedDetail.thumbnailUrl ||
|
||||
"default-image.jpg"
|
||||
);
|
||||
setDetailThumb(fileUrls);
|
||||
|
||||
|
|
@ -389,15 +397,15 @@ export default function FormImageDetail() {
|
|||
1: "Menunggu Review",
|
||||
2: "Diterima",
|
||||
3: "Minta Update",
|
||||
4: "Ditolak"
|
||||
4: "Ditolak",
|
||||
};
|
||||
return statusMap[statusId] || "Unknown";
|
||||
};
|
||||
|
||||
// Helper function to get file extension
|
||||
const getFileExtension = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
return ext || 'jpg';
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
return ext || "jpg";
|
||||
};
|
||||
|
||||
const actionApproval = (e: string) => {
|
||||
|
|
@ -565,8 +573,8 @@ export default function FormImageDetail() {
|
|||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
<Card className="w-full lg:w-8/12">
|
||||
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
|
||||
<Card className="w-full lg:w-8/12 m-2">
|
||||
<div className="px-6 py-6">
|
||||
<p className="text-lg font-semibold mb-3">Form Image</p>
|
||||
<div className="gap-5 mb-5">
|
||||
|
|
@ -611,11 +619,16 @@ export default function FormImageDetail() {
|
|||
{detail &&
|
||||
!categories?.find(
|
||||
(cat) =>
|
||||
String(cat.id) === String(detail.categoryId || detail?.category?.id)
|
||||
String(cat.id) ===
|
||||
String(detail.categoryId || detail?.category?.id)
|
||||
) && (
|
||||
<SelectItem
|
||||
key={String(detail.categoryId || detail?.category?.id)}
|
||||
value={String(detail.categoryId || detail?.category?.id)}
|
||||
key={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
)}
|
||||
value={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
)}
|
||||
>
|
||||
{detail.categoryName || detail?.category?.name}
|
||||
</SelectItem>
|
||||
|
|
@ -694,7 +707,7 @@ export default function FormImageDetail() {
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="w-full lg:w-4/12">
|
||||
<div className="w-full lg:w-4/12 m-2">
|
||||
<Card className="pb-3">
|
||||
<div className="px-3 py-3">
|
||||
<div className="space-y-2">
|
||||
|
|
@ -737,7 +750,7 @@ export default function FormImageDetail() {
|
|||
.map((tag: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
className="border rounded-md px-2 py-2"
|
||||
className="border rounded-md bg-black text-white px-2 py-2"
|
||||
>
|
||||
{tag.trim()}
|
||||
</Badge>
|
||||
|
|
@ -746,13 +759,14 @@ export default function FormImageDetail() {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
onChange={() => handleCheckboxChange(5)}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor="5">UMUM</Label>
|
||||
</div>
|
||||
|
|
@ -761,6 +775,7 @@ export default function FormImageDetail() {
|
|||
id="6"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
onChange={() => handleCheckboxChange(6)}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
|
|
@ -769,6 +784,7 @@ export default function FormImageDetail() {
|
|||
id="7"
|
||||
checked={selectedPublishers.includes(7)}
|
||||
onChange={() => handleCheckboxChange(7)}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor="7">POLRI</Label>
|
||||
</div>
|
||||
|
|
@ -777,6 +793,7 @@ export default function FormImageDetail() {
|
|||
id="8"
|
||||
checked={selectedPublishers.includes(8)}
|
||||
onChange={() => handleCheckboxChange(8)}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor="8">KSP</Label>
|
||||
</div>
|
||||
|
|
@ -789,7 +806,9 @@ export default function FormImageDetail() {
|
|||
/>
|
||||
<div className="px-3 py-3 border mx-3">
|
||||
<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>{approval?.message}</p>
|
||||
<p className="text-right text-sm">
|
||||
|
|
@ -1082,11 +1101,13 @@ export default function FormImageDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
{Number(detail?.needApprovalFromLevel || 0) == Number(userLevelId) ||
|
||||
{Number(detail?.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia == true &&
|
||||
detail?.isForwardFromNational == true &&
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -901,8 +901,8 @@ export default function FormImage() {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
<Card className="w-full lg:w-8/12">
|
||||
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
|
||||
<Card className="w-full lg:w-8/12 m-2">
|
||||
<div className="px-6 py-6">
|
||||
<p className="text-lg font-semibold mb-3">Form Image</p>
|
||||
<div className="gap-5 mb-5">
|
||||
|
|
@ -1363,7 +1363,7 @@ export default function FormImage() {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="w-full lg:w-4/12">
|
||||
<div className="w-full lg:w-4/12 m-2">
|
||||
<Card className=" h-[500px]">
|
||||
<div className="px-3 py-3">
|
||||
<div className="space-y-2">
|
||||
|
|
@ -1527,6 +1527,7 @@ export default function FormImage() {
|
|||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
|
||||
import { register } from "module";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import Cookies from "js-cookie";
|
||||
import {
|
||||
createMedia,
|
||||
deleteFile,
|
||||
deleteMedia,
|
||||
getTagsBySubCategoryId,
|
||||
listEnableCategory,
|
||||
uploadThumbnail,
|
||||
|
|
@ -106,7 +104,7 @@ export default function FormImageUpdate() {
|
|||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const { id } = useParams() as { id: string };
|
||||
console.log(id);
|
||||
console.log("INI ID NYA", id);
|
||||
const editor = useRef(null);
|
||||
type ImageSchema = z.infer<typeof imageSchema>;
|
||||
let progressInfo: any = [];
|
||||
|
|
@ -677,8 +675,7 @@ export default function FormImageUpdate() {
|
|||
<SelectValue placeholder="Pilih" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* Show the category from details if it doesn't exist in categories list */}
|
||||
{detail &&
|
||||
{detail?.category?.id &&
|
||||
!categories.find(
|
||||
(cat) =>
|
||||
String(cat.id) === String(detail.category.id)
|
||||
|
|
@ -690,6 +687,7 @@ export default function FormImageUpdate() {
|
|||
{detail.category.name}
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
{categories.map((category) => (
|
||||
<SelectItem
|
||||
key={String(category.id)}
|
||||
|
|
|
|||
|
|
@ -9,28 +9,21 @@ import { Input } from "../ui/input";
|
|||
import { Button } from "../ui/button";
|
||||
import { Label } from "../ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
||||
|
||||
// ✅ import services
|
||||
import { requestOTP, createUser } from "@/service/auth";
|
||||
|
||||
export default function SignUp() {
|
||||
const router = useRouter();
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const [step, setStep] = useState<"login" | "otp" | "form">("login");
|
||||
const [role, setRole] = useState("umum");
|
||||
const [email, setEmail] = useState("");
|
||||
const [otp, setOtp] = useState(["", "", "", "", "", ""]);
|
||||
|
||||
// role-specific
|
||||
const [membershipType, setMembershipType] = useState("");
|
||||
const [certNumber, setCertNumber] = useState("");
|
||||
const [namaTenant, setNamaTenant] = useState("");
|
||||
const [namaPerusahaan, setNamaPerusahaan] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
|
||||
// data user lengkap
|
||||
const [fullname, setFullname] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [phoneNumber, setPhoneNumber] = useState("");
|
||||
|
|
@ -40,16 +33,21 @@ export default function SignUp() {
|
|||
const [password, setPassword] = useState("");
|
||||
const [workType, setWorkType] = useState("");
|
||||
|
||||
// 🔹 Kirim OTP
|
||||
const handleSendOtp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email) {
|
||||
MySwal.fire("Error", "Email wajib diisi", "error");
|
||||
if (!email) {
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: "Email wajib diisi",
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// tentukan name sesuai role
|
||||
let name = "";
|
||||
if (role === "tenant") name = namaTenant || `${firstName} ${lastName}`;
|
||||
else if (role === "kontributor")
|
||||
|
|
@ -61,30 +59,48 @@ export default function SignUp() {
|
|||
const res = await requestOTP({ email, name });
|
||||
|
||||
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");
|
||||
} else {
|
||||
MySwal.fire("Gagal", res.message || "Gagal mengirim OTP", "error");
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal",
|
||||
text: res.message || "Gagal mengirim OTP",
|
||||
});
|
||||
}
|
||||
} catch (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) => {
|
||||
e.preventDefault();
|
||||
const code = otp.join("");
|
||||
if (code.length !== 6) {
|
||||
MySwal.fire("Error", "OTP harus 6 digit", "error");
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "OTP harus 6 digit",
|
||||
text: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
MySwal.fire("Sukses", "OTP diverifikasi!", "success");
|
||||
MySwal.fire({
|
||||
icon: "success",
|
||||
title: "Sukses",
|
||||
text: "OTP diverifikasi!",
|
||||
});
|
||||
setStep("form");
|
||||
};
|
||||
|
||||
// 🔹 Register User (API baru)
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -111,14 +127,26 @@ export default function SignUp() {
|
|||
try {
|
||||
const res = await createUser(payload);
|
||||
if (!res?.error) {
|
||||
MySwal.fire("Sukses", "Akun berhasil dibuat!", "success");
|
||||
MySwal.fire({
|
||||
icon: "success",
|
||||
title: "Sukses",
|
||||
text: "Akun berhasil dibuat!",
|
||||
});
|
||||
router.push("/auth");
|
||||
} else {
|
||||
MySwal.fire("Error", res.message || "Gagal membuat akun", "error");
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal",
|
||||
text: res.message || "Gagal membuat akun",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Register error:", err);
|
||||
MySwal.fire("Error", "Terjadi kesalahan server", "error");
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal",
|
||||
text: "Terjadi kesalahan server",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,197 @@
|
|||
import React from 'react'
|
||||
"use client";
|
||||
|
||||
const TenantUpdateForm = () => {
|
||||
return (
|
||||
<div>TenantUpdateForm</div>
|
||||
)
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const useOTP = () => {
|
|||
}, []);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -78,7 +78,7 @@ export const useOTP = () => {
|
|||
// Timer effect
|
||||
useEffect(() => {
|
||||
if (!timer.isActive || timer.countdown <= 0) {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -87,7 +87,7 @@ export const useOTP = () => {
|
|||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
countdown: Math.max(0, prev.countdown - 1000),
|
||||
}));
|
||||
|
|
@ -96,103 +96,116 @@ export const useOTP = () => {
|
|||
return () => clearInterval(interval);
|
||||
}, [timer.isActive, timer.countdown]);
|
||||
|
||||
const requestOTPCode = useCallback(async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const requestOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier);
|
||||
throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`);
|
||||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const 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 = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
category: getCategoryRoleId(category),
|
||||
};
|
||||
const verifyOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
otp: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Debug logging
|
||||
console.log("OTP Request Data:", data);
|
||||
console.log("Category before conversion:", category);
|
||||
console.log("Category after conversion:", getCategoryRoleId(category));
|
||||
if (otp.length !== 6) {
|
||||
throw new Error("OTP must be exactly 6 digits");
|
||||
}
|
||||
|
||||
const response = await requestOTP(data);
|
||||
const data = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
otp,
|
||||
category: getCategoryRoleId(category),
|
||||
};
|
||||
|
||||
if (response?.error) {
|
||||
registrationRateLimiter.recordAttempt(identifier);
|
||||
throw new Error(response.message || "Failed to send OTP");
|
||||
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]
|
||||
);
|
||||
|
||||
// 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 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]);
|
||||
const resendOTP = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
return await requestOTPCode(email, category, memberIdentity);
|
||||
},
|
||||
[requestOTPCode]
|
||||
);
|
||||
|
||||
return {
|
||||
requestOTP: requestOTPCode,
|
||||
|
|
@ -301,54 +314,60 @@ export const useInstituteData = (category?: number) => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInstitutes = useCallback(async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const fetchInstitutes = useCallback(
|
||||
async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listInstitusi(categoryId || category);
|
||||
const response = await listInstitusi(categoryId || category);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to fetch institutes");
|
||||
if (response?.error) {
|
||||
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 || []);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch institutes";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch institutes");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [category]);
|
||||
const saveInstitute = useCallback(
|
||||
async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const saveInstitute = useCallback(async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const sanitizedData = sanitizeInstituteData(instituteData);
|
||||
|
||||
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({
|
||||
name: sanitizedData.name,
|
||||
address: sanitizedData.address,
|
||||
categoryRoleId: category || 6, // Use provided category or default to Journalist category
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to save institute");
|
||||
}
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to save institute");
|
||||
return response?.data?.data?.id || 1;
|
||||
} 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;
|
||||
} 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]);
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
// Load institutes on mount if category is provided
|
||||
useEffect(() => {
|
||||
|
|
@ -371,49 +390,59 @@ export const useUserDataValidation = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateJournalistData = useCallback(async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const validateJournalistData = useCallback(
|
||||
async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await getDataJournalist(certificateNumber);
|
||||
const response = await getDataJournalist(certificateNumber);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid journalist certificate number");
|
||||
if (response?.error) {
|
||||
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;
|
||||
} 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);
|
||||
}
|
||||
}, []);
|
||||
const validatePersonnelData = useCallback(
|
||||
async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const validatePersonnelData = useCallback(async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await getDataPersonil(policeNumber);
|
||||
|
||||
const response = await getDataPersonil(policeNumber);
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid police number");
|
||||
}
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid police number");
|
||||
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 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 {
|
||||
validateJournalistData,
|
||||
|
|
@ -429,57 +458,70 @@ export const useRegistration = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submitRegistration = useCallback(async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const submitRegistration = useCallback(
|
||||
async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Sanitize and validate data
|
||||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
// Sanitize and validate data
|
||||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf);
|
||||
if (!passwordValidation.isValid) {
|
||||
throw new Error(passwordValidation.errors[0]);
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(
|
||||
sanitizedData.password,
|
||||
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
|
||||
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]);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return {
|
||||
submitRegistration,
|
||||
|
|
@ -490,78 +532,90 @@ export const useRegistration = () => {
|
|||
|
||||
// Hook for form validation
|
||||
export const useFormValidation = () => {
|
||||
const validateIdentityForm = useCallback((
|
||||
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
}, []);
|
||||
const validateIdentityForm = useCallback(
|
||||
(
|
||||
data:
|
||||
| JournalistRegistrationData
|
||||
| PersonnelRegistrationData
|
||||
| GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
const validateProfileForm = useCallback(
|
||||
(data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate required fields
|
||||
if (!data.firstName?.trim()) {
|
||||
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!);
|
||||
// Validate required fields
|
||||
if (!data.firstName?.trim()) {
|
||||
errors.push("Full name is required");
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Email is required");
|
||||
} else {
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
if (!data.username?.trim()) {
|
||||
errors.push("Username is required");
|
||||
} else {
|
||||
const usernameValidation = validateUsername(data.username);
|
||||
if (!usernameValidation.isValid) {
|
||||
errors.push(usernameValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.phoneNumber?.trim()) {
|
||||
errors.push("Phone number is required");
|
||||
} else {
|
||||
const phoneValidation = validatePhoneNumber(data.phoneNumber);
|
||||
if (!phoneValidation.isValid) {
|
||||
errors.push(phoneValidation.error!);
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Email is required");
|
||||
} else {
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.address?.trim()) {
|
||||
errors.push("Address is required");
|
||||
}
|
||||
|
||||
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]);
|
||||
if (!data.phoneNumber?.trim()) {
|
||||
errors.push("Phone number is required");
|
||||
} else {
|
||||
const phoneValidation = validatePhoneNumber(data.phoneNumber);
|
||||
if (!phoneValidation.isValid) {
|
||||
errors.push(phoneValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}, []);
|
||||
if (!data.address?.trim()) {
|
||||
errors.push("Address is required");
|
||||
}
|
||||
|
||||
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 {
|
||||
validateIdentityForm,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const useOTP = () => {
|
|||
}, []);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -78,7 +78,7 @@ export const useOTP = () => {
|
|||
// Timer effect
|
||||
useEffect(() => {
|
||||
if (!timer.isActive || timer.countdown <= 0) {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -87,7 +87,7 @@ export const useOTP = () => {
|
|||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
countdown: Math.max(0, prev.countdown - 1000),
|
||||
}));
|
||||
|
|
@ -96,103 +96,116 @@ export const useOTP = () => {
|
|||
return () => clearInterval(interval);
|
||||
}, [timer.isActive, timer.countdown]);
|
||||
|
||||
const requestOTPCode = useCallback(async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const requestOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier);
|
||||
throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`);
|
||||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const 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 = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
category: getCategoryRoleId(category),
|
||||
};
|
||||
const verifyOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
otp: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Debug logging
|
||||
console.log("OTP Request Data:", data);
|
||||
console.log("Category before conversion:", category);
|
||||
console.log("Category after conversion:", getCategoryRoleId(category));
|
||||
if (otp.length !== 6) {
|
||||
throw new Error("OTP must be exactly 6 digits");
|
||||
}
|
||||
|
||||
const response = await requestOTP(data);
|
||||
const data = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
otp,
|
||||
category: getCategoryRoleId(category),
|
||||
};
|
||||
|
||||
if (response?.error) {
|
||||
registrationRateLimiter.recordAttempt(identifier);
|
||||
throw new Error(response.message || "Failed to send OTP");
|
||||
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]
|
||||
);
|
||||
|
||||
// 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 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]);
|
||||
const resendOTP = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
return await requestOTPCode(email, category, memberIdentity);
|
||||
},
|
||||
[requestOTPCode]
|
||||
);
|
||||
|
||||
return {
|
||||
requestOTP: requestOTPCode,
|
||||
|
|
@ -301,54 +314,60 @@ export const useInstituteData = (category?: number) => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInstitutes = useCallback(async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const fetchInstitutes = useCallback(
|
||||
async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listInstitusi(categoryId || category);
|
||||
const response = await listInstitusi(categoryId || category);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to fetch institutes");
|
||||
if (response?.error) {
|
||||
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 || []);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch institutes";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch institutes");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [category]);
|
||||
const saveInstitute = useCallback(
|
||||
async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const saveInstitute = useCallback(async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const sanitizedData = sanitizeInstituteData(instituteData);
|
||||
|
||||
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({
|
||||
name: sanitizedData.name,
|
||||
address: sanitizedData.address,
|
||||
categoryRoleId: category || 6, // Use provided category or default to Journalist category
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to save institute");
|
||||
}
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to save institute");
|
||||
return response?.data?.data?.id || 1;
|
||||
} 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;
|
||||
} 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]);
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
// Load institutes on mount if category is provided
|
||||
useEffect(() => {
|
||||
|
|
@ -371,49 +390,59 @@ export const useUserDataValidation = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateJournalistData = useCallback(async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const validateJournalistData = useCallback(
|
||||
async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await getDataJournalist(certificateNumber);
|
||||
const response = await getDataJournalist(certificateNumber);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid journalist certificate number");
|
||||
if (response?.error) {
|
||||
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;
|
||||
} 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);
|
||||
}
|
||||
}, []);
|
||||
const validatePersonnelData = useCallback(
|
||||
async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const validatePersonnelData = useCallback(async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await getDataPersonil(policeNumber);
|
||||
|
||||
const response = await getDataPersonil(policeNumber);
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid police number");
|
||||
}
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid police number");
|
||||
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 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 {
|
||||
validateJournalistData,
|
||||
|
|
@ -429,57 +458,70 @@ export const useRegistration = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submitRegistration = useCallback(async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const submitRegistration = useCallback(
|
||||
async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Sanitize and validate data
|
||||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
// Sanitize and validate data
|
||||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf);
|
||||
if (!passwordValidation.isValid) {
|
||||
throw new Error(passwordValidation.errors[0]);
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(
|
||||
sanitizedData.password,
|
||||
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
|
||||
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]);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return {
|
||||
submitRegistration,
|
||||
|
|
@ -490,78 +532,90 @@ export const useRegistration = () => {
|
|||
|
||||
// Hook for form validation
|
||||
export const useFormValidation = () => {
|
||||
const validateIdentityForm = useCallback((
|
||||
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
}, []);
|
||||
const validateIdentityForm = useCallback(
|
||||
(
|
||||
data:
|
||||
| JournalistRegistrationData
|
||||
| PersonnelRegistrationData
|
||||
| GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
const validateProfileForm = useCallback(
|
||||
(data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate required fields
|
||||
if (!data.firstName?.trim()) {
|
||||
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!);
|
||||
// Validate required fields
|
||||
if (!data.firstName?.trim()) {
|
||||
errors.push("Full name is required");
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Email is required");
|
||||
} else {
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
if (!data.username?.trim()) {
|
||||
errors.push("Username is required");
|
||||
} else {
|
||||
const usernameValidation = validateUsername(data.username);
|
||||
if (!usernameValidation.isValid) {
|
||||
errors.push(usernameValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.phoneNumber?.trim()) {
|
||||
errors.push("Phone number is required");
|
||||
} else {
|
||||
const phoneValidation = validatePhoneNumber(data.phoneNumber);
|
||||
if (!phoneValidation.isValid) {
|
||||
errors.push(phoneValidation.error!);
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Email is required");
|
||||
} else {
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.address?.trim()) {
|
||||
errors.push("Address is required");
|
||||
}
|
||||
|
||||
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]);
|
||||
if (!data.phoneNumber?.trim()) {
|
||||
errors.push("Phone number is required");
|
||||
} else {
|
||||
const phoneValidation = validatePhoneNumber(data.phoneNumber);
|
||||
if (!phoneValidation.isValid) {
|
||||
errors.push(phoneValidation.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}, []);
|
||||
if (!data.address?.trim()) {
|
||||
errors.push("Address is required");
|
||||
}
|
||||
|
||||
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 {
|
||||
validateIdentityForm,
|
||||
|
|
|
|||
38
lib/swal.ts
38
lib/swal.ts
|
|
@ -90,3 +90,41 @@ export function successToast(title: string, text: string) {
|
|||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPostInterceptor,
|
||||
httpPutInterceptor,
|
||||
} from "../http-config/http-interceptor-service";
|
||||
|
||||
interface GetCategoriesParams {
|
||||
|
|
@ -80,8 +81,20 @@ export async function createCategories(data: CreateCategoryPayload) {
|
|||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
|
||||
export async function getArticleCategoryDetail(id: number) {
|
||||
const url = `article-categories/${id}`;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,6 +314,10 @@ export async function deleteMedia(data: any) {
|
|||
return httpDeleteInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function deleteArticle(id: number) {
|
||||
const url = `articles/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
export async function deleteFile(data: any) {
|
||||
const url = "media/file";
|
||||
return httpDeleteInterceptor(url, data);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
const url = `user-levels/${id}`;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue