fix: fixing bugs in userlevels
This commit is contained in:
parent
b6bde7722e
commit
fc1d4a6b9a
|
|
@ -17,6 +17,7 @@ import { deleteArticle, deleteMedia } from "@/service/content/content";
|
|||
import { error } from "@/lib/swal";
|
||||
import Swal from "sweetalert2";
|
||||
import Link from "next/link";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -137,57 +138,119 @@ const useTableColumns = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const statusColors: Record<string, string> = {
|
||||
diterima: "bg-green-100 text-green-600",
|
||||
"menunggu review": "bg-orange-100 text-orange-600",
|
||||
const {
|
||||
statusId,
|
||||
statusName,
|
||||
isPublish,
|
||||
reviewedAtLevel = "",
|
||||
creatorGroupLevelId,
|
||||
needApprovalFromLevel,
|
||||
} = row.original;
|
||||
|
||||
const userLevelId = Number(getCookiesDecrypt("ulie"));
|
||||
|
||||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
|
||||
if (isPublish) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge className="flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 bg-green-100 text-green-700 text-center whitespace-nowrap">
|
||||
Published
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let label = statusName || "Menunggu Review";
|
||||
|
||||
if (statusId === 2 && !userHasReviewed && !isCreator) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 1 && needApprovalFromLevel === userLevelId) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 2) {
|
||||
label = "Diterima";
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
"Menunggu Review": "bg-orange-100 text-orange-600",
|
||||
Diterima: "bg-blue-100 text-blue-600",
|
||||
Published: "bg-green-100 text-green-700",
|
||||
Unknown: "bg-gray-100 text-gray-600",
|
||||
default: "bg-gray-100 text-gray-600",
|
||||
};
|
||||
|
||||
const colors = [
|
||||
"bg-orange-100 text-orange-600",
|
||||
"bg-orange-100 text-orange-600",
|
||||
"bg-green-100 text-green-600",
|
||||
"bg-blue-100 text-blue-600",
|
||||
"bg-red-200 text-red-600",
|
||||
];
|
||||
|
||||
const status =
|
||||
Number(row.original?.statusId) == 2 &&
|
||||
row.original?.reviewedAtLevel !== null &&
|
||||
!row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
|
||||
Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
|
||||
? "1"
|
||||
: row.original?.statusId;
|
||||
const statusStyles =
|
||||
colors[Number(status)] || "bg-red-200 text-red-600";
|
||||
// const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-full px-5 w-full whitespace-nowrap",
|
||||
statusStyles
|
||||
)}
|
||||
>
|
||||
{(Number(row.original?.statusId) == 2 &&
|
||||
!row.original?.reviewedAtLevel !== null &&
|
||||
!row.original?.reviewedAtLevel?.includes(
|
||||
`:${Number(userLevelId)}:`
|
||||
) &&
|
||||
Number(row.original?.creatorGroupLevelId) !=
|
||||
Number(userLevelId)) ||
|
||||
(Number(row.original?.statusId) == 1 &&
|
||||
Number(row.original?.needApprovalFromLevel) ==
|
||||
Number(userLevelId))
|
||||
? "Menunggu Review"
|
||||
: row.original?.statusName}{" "}
|
||||
</Badge>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge
|
||||
className={cn(
|
||||
"flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 text-center whitespace-nowrap",
|
||||
colors[label] || colors.default,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// accessorKey: "statusName",
|
||||
// header: "Status",
|
||||
// cell: ({ row }) => {
|
||||
// const statusColors: Record<string, string> = {
|
||||
// diterima: "bg-green-100 text-green-600",
|
||||
// "menunggu review": "bg-orange-100 text-orange-600",
|
||||
// };
|
||||
|
||||
// const colors = [
|
||||
// "bg-orange-100 text-orange-600",
|
||||
// "bg-orange-100 text-orange-600",
|
||||
// "bg-green-100 text-green-600",
|
||||
// "bg-blue-100 text-blue-600",
|
||||
// "bg-red-200 text-red-600",
|
||||
// ];
|
||||
|
||||
// const status =
|
||||
// Number(row.original?.statusId) == 2 &&
|
||||
// row.original?.reviewedAtLevel !== null &&
|
||||
// !row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
|
||||
// Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
|
||||
// ? "1"
|
||||
// : row.original?.statusId;
|
||||
// const statusStyles =
|
||||
// colors[Number(status)] || "bg-red-200 text-red-600";
|
||||
// // const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
|
||||
|
||||
// return (
|
||||
// <Badge
|
||||
// className={cn(
|
||||
// "rounded-full px-5 w-full whitespace-nowrap",
|
||||
// statusStyles
|
||||
// )}
|
||||
// >
|
||||
// {(Number(row.original?.statusId) == 2 &&
|
||||
// !row.original?.reviewedAtLevel !== null &&
|
||||
// !row.original?.reviewedAtLevel?.includes(
|
||||
// `:${Number(userLevelId)}:`
|
||||
// ) &&
|
||||
// Number(row.original?.creatorGroupLevelId) !=
|
||||
// Number(userLevelId)) ||
|
||||
// (Number(row.original?.statusId) == 1 &&
|
||||
// Number(row.original?.needApprovalFromLevel) ==
|
||||
// Number(userLevelId))
|
||||
// ? "Menunggu Review"
|
||||
// : row.original?.statusName}{" "}
|
||||
// </Badge>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
|
|
@ -241,7 +304,7 @@ const useTableColumns = () => {
|
|||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3,
|
||||
);
|
||||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
|
@ -258,12 +321,15 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/content/audio/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<AccessGuard action="view">
|
||||
<Link href={`/admin/content/audio/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
|
||||
{/* <Link
|
||||
href={`/admin/content/audio/update/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -272,22 +338,31 @@ const useTableColumns = () => {
|
|||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link> */}
|
||||
{(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && (
|
||||
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
|
||||
<AccessGuard action="edit">
|
||||
<Link href={`/admin/content/audio/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group 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"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
|
||||
{/* )} */}
|
||||
|
||||
<AccessGuard action="delete">
|
||||
<DropdownMenuItem
|
||||
onClick={() => 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 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
|
||||
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import AudioTabs from "./components/audio-tabs";
|
|||
import { UploadIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { usePermission } from "@/components/context/permission-context";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const ReactTableAudioPage = () => {
|
||||
const { can } = usePermission();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
|
|
@ -21,18 +25,27 @@ const ReactTableAudioPage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Audio Management</h1>
|
||||
<p className="text-sm text-gray-500">Manage your submitted audio files and pending approvals</p>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Audio Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage your submitted audio files and pending approvals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/audio/create"}>
|
||||
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Audio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<AccessGuard action="create">
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/audio/create"}>
|
||||
<Button
|
||||
color="primary"
|
||||
className="text-white shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Audio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -46,4 +59,4 @@ const ReactTableAudioPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ReactTableAudioPage;
|
||||
export default ReactTableAudioPage;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import Swal from "sweetalert2";
|
|||
import withReactContent from "sweetalert2-react-content";
|
||||
import Link from "next/link";
|
||||
import { deleteArticle } from "@/service/content/content";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -183,7 +184,6 @@ const useTableColumns = () => {
|
|||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
|
||||
|
||||
if (isPublish) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
|
|
@ -193,7 +193,7 @@ const useTableColumns = () => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
let label = statusName || "Menunggu Review";
|
||||
|
||||
if (statusId === 2 && !userHasReviewed && !isCreator) {
|
||||
|
|
@ -217,7 +217,7 @@ const useTableColumns = () => {
|
|||
<Badge
|
||||
className={cn(
|
||||
"flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 text-center whitespace-nowrap",
|
||||
colors[label] || colors.default
|
||||
colors[label] || colors.default,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
|
|
@ -281,7 +281,7 @@ const useTableColumns = () => {
|
|||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
Number(userLevelId) === 216 && Number(roleId) === 3
|
||||
Number(userLevelId) === 216 && Number(roleId) === 3,
|
||||
);
|
||||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
|
@ -298,30 +298,38 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 hover:text-black" align="end">
|
||||
<Link
|
||||
href={`/admin/content/image/detail/${row.original.id}`}
|
||||
className="hover:text-black"
|
||||
>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none cursor-pointer hover:bg-slate-200">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<AccessGuard action="view">
|
||||
<Link
|
||||
href={`/admin/content/image/detail/${row.original.id}`}
|
||||
className="hover:text-black"
|
||||
>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none cursor-pointer hover:bg-slate-200">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
{/* {(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 cursor-pointer hover:bg-slate-200">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<AccessGuard action="edit">
|
||||
<Link href={`/admin/content/image/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none cursor-pointer hover:bg-slate-200">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
|
||||
{/* )} */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
|
||||
<AccessGuard action="delete">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import ImageTabs from "./components/image-tabs";
|
|||
import { UploadIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { usePermission } from "@/components/context/permission-context";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const ReactTableImagePage = () => {
|
||||
const { can } = usePermission();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
|
|
@ -21,24 +25,33 @@ const ReactTableImagePage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Image Management</h1>
|
||||
<p className="text-sm text-gray-500">Manage your submitted images and pending approvals</p>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Image Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage your submitted images and pending approvals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/image/create"}>
|
||||
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow cursor-pointer">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Image
|
||||
</Button>
|
||||
</Link>
|
||||
{/* <Link href={"/contributor/content/image/createAi"}>
|
||||
<AccessGuard action="create">
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/image/create"}>
|
||||
<Button
|
||||
color="primary"
|
||||
className="text-white shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Image
|
||||
</Button>
|
||||
</Link>
|
||||
{/* <Link href={"/contributor/content/image/createAi"}>
|
||||
<Button color="primary" className="text-white ml-3">
|
||||
<UploadIcon />
|
||||
Unggah Foto Dengan AI
|
||||
</Button>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { deleteArticle, deleteMedia } from "@/service/content/content";
|
|||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
import Link from "next/link";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -142,50 +143,59 @@ const useTableColumns = () => {
|
|||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const statusColors: Record<string, string> = {
|
||||
diterima: "bg-green-100 text-green-600",
|
||||
"menunggu review": "bg-orange-100 text-orange-600",
|
||||
const {
|
||||
statusId,
|
||||
statusName,
|
||||
isPublish,
|
||||
reviewedAtLevel = "",
|
||||
creatorGroupLevelId,
|
||||
needApprovalFromLevel,
|
||||
} = row.original;
|
||||
|
||||
const userLevelId = Number(getCookiesDecrypt("ulie"));
|
||||
|
||||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
|
||||
if (isPublish) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge className="flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 bg-green-100 text-green-700 text-center whitespace-nowrap">
|
||||
Published
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let label = statusName || "Menunggu Review";
|
||||
|
||||
if (statusId === 2 && !userHasReviewed && !isCreator) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 1 && needApprovalFromLevel === userLevelId) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 2) {
|
||||
label = "Diterima";
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
"Menunggu Review": "bg-orange-100 text-orange-600",
|
||||
Diterima: "bg-blue-100 text-blue-600",
|
||||
Published: "bg-green-100 text-green-700",
|
||||
Unknown: "bg-gray-100 text-gray-600",
|
||||
default: "bg-gray-100 text-gray-600",
|
||||
};
|
||||
|
||||
const colors = [
|
||||
"bg-orange-100 text-orange-600",
|
||||
"bg-orange-100 text-orange-600",
|
||||
"bg-green-100 text-green-600",
|
||||
"bg-blue-100 text-blue-600",
|
||||
"bg-red-200 text-red-600",
|
||||
];
|
||||
|
||||
const status =
|
||||
Number(row.original?.statusId) == 2 &&
|
||||
row.original?.reviewedAtLevel !== null &&
|
||||
!row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
|
||||
Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
|
||||
? "1"
|
||||
: row.original?.statusId;
|
||||
const statusStyles =
|
||||
colors[Number(status)] || "bg-red-200 text-red-600";
|
||||
// const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-full px-5 w-full whitespace-nowrap",
|
||||
statusStyles
|
||||
)}
|
||||
>
|
||||
{(Number(row.original?.statusId) == 2 &&
|
||||
!row.original?.reviewedAtLevel !== null &&
|
||||
!row.original?.reviewedAtLevel?.includes(
|
||||
`:${Number(userLevelId)}:`
|
||||
) &&
|
||||
Number(row.original?.creatorGroupLevelId) !=
|
||||
Number(userLevelId)) ||
|
||||
(Number(row.original?.statusId) == 1 &&
|
||||
Number(row.original?.needApprovalFromLevel) ==
|
||||
Number(userLevelId))
|
||||
? "Menunggu Review"
|
||||
: row.original?.statusName}{" "}
|
||||
</Badge>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge
|
||||
className={cn(
|
||||
"flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 text-center whitespace-nowrap",
|
||||
colors[label] || colors.default,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -242,7 +252,7 @@ const useTableColumns = () => {
|
|||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3,
|
||||
);
|
||||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
|
@ -259,28 +269,37 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/content/text/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && (
|
||||
<Link href={`/admin/content/text/update/${row.original.id}`}>
|
||||
<AccessGuard action="view">
|
||||
<Link href={`/admin/content/text/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
|
||||
<AccessGuard action="edit">
|
||||
<Link href={`/admin/content/text/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group 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"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
|
||||
{/* )} */}
|
||||
<AccessGuard action="delete">
|
||||
<DropdownMenuItem
|
||||
onClick={() => 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 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import DocumentTabs from "./components/document-tabs";
|
|||
import { UploadIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { usePermission } from "@/components/context/permission-context";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const ReactTableDocumentPage = () => {
|
||||
const { can } = usePermission();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
|
|
@ -21,18 +25,28 @@ const ReactTableDocumentPage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Document Management</h1>
|
||||
<p className="text-sm text-gray-500">Manage your submitted documents and pending approvals</p>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Document Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage your submitted documents and pending approvals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/text/create"}>
|
||||
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Document
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<AccessGuard action="create">
|
||||
{" "}
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/text/create"}>
|
||||
<Button
|
||||
color="primary"
|
||||
className="text-white shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Document
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { error } from "@/lib/swal";
|
|||
import Link from "next/link";
|
||||
import { deleteMedia } from "@/service/content";
|
||||
import { deleteArticle } from "@/service/content/content";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -194,45 +195,59 @@ const useTableColumns = () => {
|
|||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const statusId = Number(row.original?.statusId);
|
||||
const reviewedAtLevel = row.original?.reviewedAtLevel || "";
|
||||
const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
|
||||
const needApprovalFromLevel = Number(
|
||||
row.original?.needApprovalFromLevel
|
||||
);
|
||||
const {
|
||||
statusId,
|
||||
statusName,
|
||||
isPublish,
|
||||
reviewedAtLevel = "",
|
||||
creatorGroupLevelId,
|
||||
needApprovalFromLevel,
|
||||
} = row.original;
|
||||
|
||||
const userLevelId = Number(getCookiesDecrypt("ulie"));
|
||||
|
||||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = creatorGroupLevelId === Number(userLevelId);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
|
||||
const isWaitingForReview =
|
||||
statusId === 2 && !userHasReviewed && !isCreator;
|
||||
const isApprovalNeeded =
|
||||
statusId === 1 && needApprovalFromLevel === Number(userLevelId);
|
||||
if (isPublish) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge className="flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 bg-green-100 text-green-700 text-center whitespace-nowrap">
|
||||
Published
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const label =
|
||||
isWaitingForReview || isApprovalNeeded
|
||||
? "Menunggu Review"
|
||||
: statusId === 2
|
||||
? "Diterima"
|
||||
: row.original?.statusName;
|
||||
let label = statusName || "Menunggu Review";
|
||||
|
||||
if (statusId === 2 && !userHasReviewed && !isCreator) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 1 && needApprovalFromLevel === userLevelId) {
|
||||
label = "Menunggu Review";
|
||||
} else if (statusId === 2) {
|
||||
label = "Diterima";
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
"Menunggu Review": "bg-orange-100 text-orange-600",
|
||||
Diterima: "bg-green-100 text-green-600",
|
||||
default: "bg-red-200 text-red-600",
|
||||
Diterima: "bg-blue-100 text-blue-600",
|
||||
Published: "bg-green-100 text-green-700",
|
||||
Unknown: "bg-gray-100 text-gray-600",
|
||||
default: "bg-gray-100 text-gray-600",
|
||||
};
|
||||
|
||||
const statusStyles = colors[label] || colors.default;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-full px-5 w-full whitespace-nowrap",
|
||||
statusStyles
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Badge
|
||||
className={cn(
|
||||
"flex items-center justify-center min-w-[120px] rounded-full px-5 py-1 text-center whitespace-nowrap",
|
||||
colors[label] || colors.default,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -289,7 +304,7 @@ const useTableColumns = () => {
|
|||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3
|
||||
Number(userLevelId) == 216 && Number(roleId) == 3,
|
||||
);
|
||||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
|
@ -306,12 +321,15 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/admin/content/video/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<AccessGuard action="view">
|
||||
<Link href={`/admin/content/video/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
|
||||
{/* <Link
|
||||
href={`/contributor/content/video/update/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -322,20 +340,27 @@ const useTableColumns = () => {
|
|||
</Link> */}
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
<Link href={`/admin/content/video/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
|
||||
<AccessGuard action="edit">
|
||||
<Link href={`/admin/content/video/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</AccessGuard>
|
||||
{/* )} */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => 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 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
|
||||
<AccessGuard action="delete">
|
||||
<DropdownMenuItem
|
||||
onClick={() => 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 focus:text-white" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AccessGuard>
|
||||
|
||||
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
|
|
|
|||
|
|
@ -72,18 +72,14 @@ const TableVideo = () => {
|
|||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [showData, setShowData] = React.useState("50");
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
});
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [limit, setLimit] = React.useState(10);
|
||||
const [search, setSearch] = React.useState<string>("");
|
||||
const userId = getCookiesDecrypt("uie");
|
||||
const userLevelId = getCookiesDecrypt("ulie");
|
||||
|
||||
const [categories, setCategories] = React.useState<any[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
|
||||
[]
|
||||
|
|
@ -95,7 +91,6 @@ const TableVideo = () => {
|
|||
const [filterByCreator, setFilterByCreator] = React.useState("");
|
||||
const [filterBySource, setFilterBySource] = React.useState("");
|
||||
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
const columns = useTableColumns();
|
||||
const table = useReactTable({
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import AudioVisualTabs from "./components/audio-visual-tabs";
|
|||
import { UploadIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { usePermission } from "@/components/context/permission-context";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const ReactTableAudioVisualPage = () => {
|
||||
const { can } = usePermission();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
|
|
@ -17,22 +21,32 @@ const ReactTableAudioVisualPage = () => {
|
|||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<div className="w-8 h-8 bg-black rounded-lg flex items-center justify-center">
|
||||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Audio-Visual Management</h1>
|
||||
<p className="text-sm text-gray-500">Manage your submitted audio-visual files and pending approvals</p>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Audio-Visual Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Manage your submitted audio-visual files and pending
|
||||
approvals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/video/create"}>
|
||||
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Audio-Visual
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<AccessGuard action="create">
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/video/create"}>
|
||||
<Button
|
||||
color="primary"
|
||||
className="text-white shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
Create Audio-Visual
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -46,4 +60,4 @@ const ReactTableAudioVisualPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ReactTableAudioVisualPage;
|
||||
export default ReactTableAudioVisualPage;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function ManagementUser() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<section className="flex flex-col gap-2 bg-slate-50 dark:bg-black rounded-lg p-3 mt-5 border">
|
||||
<section className="flex flex-col gap-2 bg-white dark:bg-slate-200 rounded-lg p-3 mt-5 border">
|
||||
<div className="flex justify-between py-3">
|
||||
<p className="text-lg">
|
||||
Data User
|
||||
|
|
|
|||
|
|
@ -6,21 +6,52 @@ import ThemeCustomize from "@/components/partials/customizer";
|
|||
import DashCodeHeader from "@/components/partials/header";
|
||||
import MountedProvider from "@/providers/mounted.provider";
|
||||
import { WorkflowModalProvider } from "@/components/modals/WorkflowModalProvider";
|
||||
import { PermissionProvider } from "@/components/context/permission-context";
|
||||
|
||||
const layout = async ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<MountedProvider isProtected={true}>
|
||||
<LayoutProvider>
|
||||
<WorkflowModalProvider>
|
||||
<ThemeCustomize />
|
||||
<DashCodeHeader />
|
||||
<DashCodeSidebar />
|
||||
<LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
<DashCodeFooter />
|
||||
</WorkflowModalProvider>
|
||||
</LayoutProvider>
|
||||
{/* 🔐 Permission hanya untuk admin */}
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<WorkflowModalProvider>
|
||||
<ThemeCustomize />
|
||||
<DashCodeHeader />
|
||||
<DashCodeSidebar />
|
||||
<LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
<DashCodeFooter />
|
||||
</WorkflowModalProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
</MountedProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default layout;
|
||||
|
||||
// import LayoutProvider from "@/providers/layout.provider";
|
||||
// import LayoutContentProvider from "@/providers/content.provider";
|
||||
// import DashCodeSidebar from "@/components/partials/sidebar";
|
||||
// import DashCodeFooter from "@/components/partials/footer";
|
||||
// import ThemeCustomize from "@/components/partials/customizer";
|
||||
// import DashCodeHeader from "@/components/partials/header";
|
||||
// import MountedProvider from "@/providers/mounted.provider";
|
||||
// import { WorkflowModalProvider } from "@/components/modals/WorkflowModalProvider";
|
||||
|
||||
// const layout = async ({ children }: { children: React.ReactNode }) => {
|
||||
// return (
|
||||
// <MountedProvider isProtected={true}>
|
||||
// <LayoutProvider>
|
||||
// <WorkflowModalProvider>
|
||||
// <ThemeCustomize />
|
||||
// <DashCodeHeader />
|
||||
// <DashCodeSidebar />
|
||||
// <LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
// <DashCodeFooter />
|
||||
// </WorkflowModalProvider>
|
||||
// </LayoutProvider>
|
||||
// </MountedProvider>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default layout;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { usePermission } from "./context/permission-context";
|
||||
|
||||
export function AccessGuard({
|
||||
module,
|
||||
action,
|
||||
children,
|
||||
fallback = null,
|
||||
}: {
|
||||
module?: string;
|
||||
action?: string;
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}) {
|
||||
const { canModule, canAction, can, loading } = usePermission();
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
// ❗ WAJIB ADA RULE
|
||||
if (!module && !action) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.warn("AccessGuard requires module and/or action");
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (module && action && !can(module, action)) return fallback;
|
||||
if (module && !action && !canModule(module)) return fallback;
|
||||
if (!module && action && !canAction(action)) return fallback;
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import { getUserLevelModuleAccessesByUserLevelId } from "@/service/user-level-module-accesses";
|
||||
import { getUserLevelMenuActionAccesses } from "@/service/user-level-menu-action-accesses";
|
||||
import { getUserInfo } from "@/service/user";
|
||||
|
||||
type ModuleAccessMap = Record<string, boolean>;
|
||||
type ActionAccessMap = Record<string, string[]>;
|
||||
|
||||
interface PermissionContextType {
|
||||
canModule: (moduleCode: string) => boolean;
|
||||
canAction: (actionCode: string) => boolean;
|
||||
can: (moduleCode: string, actionCode: string) => boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const PermissionContext = createContext<PermissionContextType | null>(null);
|
||||
|
||||
export const PermissionProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [moduleAccess, setModuleAccess] = useState<ModuleAccessMap>({});
|
||||
const [actionCodes, setActionCodes] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPermissions = async () => {
|
||||
try {
|
||||
// ✅ SUMBER KEBENARAN: API USER INFO
|
||||
const userRes = await getUserInfo();
|
||||
const userLevelId = userRes?.data?.data?.userLevelId;
|
||||
|
||||
console.log("USER LEVEL ID FROM API:", userLevelId);
|
||||
|
||||
if (!userLevelId) {
|
||||
console.error("userLevelId not found from users/info");
|
||||
return;
|
||||
}
|
||||
|
||||
const [moduleRes, actionRes] = await Promise.all([
|
||||
getUserLevelModuleAccessesByUserLevelId(userLevelId),
|
||||
getUserLevelMenuActionAccesses({
|
||||
userLevelId,
|
||||
canAccess: true,
|
||||
limit: 10000,
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log("ACTION ACCESS RAW:", actionRes?.data?.data);
|
||||
|
||||
// 🔹 MODULE ACCESS
|
||||
// MODULE ACCESS
|
||||
const moduleMap: Record<string, boolean> = {};
|
||||
moduleRes?.data?.data?.forEach((item: any) => {
|
||||
if (
|
||||
item.module?.code &&
|
||||
item.canAccess === true &&
|
||||
item.isActive !== false
|
||||
) {
|
||||
moduleMap[item.module.code] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// ACTION ACCESS
|
||||
const actions =
|
||||
actionRes?.data?.data
|
||||
?.filter(
|
||||
(item: any) =>
|
||||
Number(item.userLevelId) === Number(userLevelId) &&
|
||||
item.canAccess === true &&
|
||||
item.isActive === true,
|
||||
)
|
||||
.map((item: any) => item.actionCode) ?? [];
|
||||
|
||||
setModuleAccess(moduleMap);
|
||||
setActionCodes(actions);
|
||||
} catch (error) {
|
||||
console.error("Failed to load permissions", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPermissions();
|
||||
}, []);
|
||||
|
||||
const canModule = (moduleCode: string) => moduleAccess[moduleCode] === true;
|
||||
|
||||
const canAction = (actionCode: string) => actionCodes.includes(actionCode);
|
||||
|
||||
/**
|
||||
* FINAL GUARD
|
||||
* - harus punya module
|
||||
* - harus punya action
|
||||
*/
|
||||
const can = (moduleCode: any, actionCode: any) =>
|
||||
canModule(moduleCode) && canAction(actionCode);
|
||||
|
||||
return (
|
||||
<PermissionContext.Provider value={{ canModule, canAction, can, loading }}>
|
||||
{children}
|
||||
</PermissionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePermission = () => {
|
||||
const ctx = useContext(PermissionContext);
|
||||
if (!ctx) {
|
||||
throw new Error("usePermission must be used inside PermissionProvider");
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -48,6 +48,7 @@ import dynamic from "next/dynamic";
|
|||
import { formatDateToIndonesian } from "@/utils/globals";
|
||||
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
|
||||
import { listArticleCategories } from "@/service/content";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const videoSchema = z.object({
|
||||
title: z.string().min(1, { message: "Judul diperlukan" }),
|
||||
|
|
@ -198,7 +199,7 @@ export default function FormVideoDetail() {
|
|||
|
||||
const handleCheckboxChange = (id: number) => {
|
||||
setSelectedPublishers((prev) =>
|
||||
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -234,6 +235,15 @@ export default function FormVideoDetail() {
|
|||
|
||||
if (!detail) return <div className="p-10 text-gray-500">Memuat data...</div>;
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
const isApproved = Number(detail?.statusId) === 2;
|
||||
const isRejected = Number(detail?.statusId) === 4;
|
||||
|
||||
const isCreator =
|
||||
Number(detail?.createdById || detail?.uploadedById) === Number(userId);
|
||||
|
||||
const hasApproval = approval != null;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
|
||||
|
|
@ -325,7 +335,7 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
|
||||
<div className="mt-3 px-3 space-y-2">
|
||||
<Label>Preview</Label>
|
||||
<Label>Thumbnail</Label>
|
||||
<Card className="mt-2 w-fit">
|
||||
<img
|
||||
src={detail.thumbnailUrl || detail.thumbnailLink}
|
||||
|
|
@ -387,7 +397,46 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{(Number(detail.needApprovalFromLevel || 0) ==
|
||||
{/* {isPending &&
|
||||
(Number(detail.needApprovalFromLevel || 0) ===
|
||||
Number(userLevelId) ||
|
||||
detail.isPublish === false) &&
|
||||
Number(detail.uploadedById || detail.createdById) !==
|
||||
Number(userId) ? ( */}
|
||||
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:check" className="mr-3" /> Accept
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => actionApproval("3")}
|
||||
className="bg-orange-400 hover:bg-orange-300"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:comment-o" className="mr-3" /> Revision
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => actionApproval("4")}
|
||||
color="destructive"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:times" className="mr-3" /> Reject
|
||||
</Button>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
)}
|
||||
|
||||
{/* ) : null} */}
|
||||
|
||||
{/* {(Number(detail.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail.isPublish === false && detail.statusId == 1)) &&
|
||||
Number(detail.uploadedById || detail.createdById) !=
|
||||
|
|
@ -415,7 +464,7 @@ export default function FormVideoDetail() {
|
|||
<Icon icon="fa:times" className="mr-3" /> Reject
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
) : null} */}
|
||||
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="max-h-[600px] overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import AudioPlayer from "@/components/audio-player";
|
||||
import { listArticleCategories } from "@/service/content";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const imageSchema = z.object({
|
||||
title: z.string().min(1, { message: "Judul diperlukan" }),
|
||||
|
|
@ -112,7 +113,7 @@ const ViewEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormAudioDetail() {
|
||||
|
|
@ -219,7 +220,7 @@ export default function FormAudioDetail() {
|
|||
|
||||
const handleCheckboxChange = (id: number) => {
|
||||
setSelectedPublishers((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -249,9 +250,7 @@ export default function FormAudioDetail() {
|
|||
if (id) {
|
||||
const response = await getArticleDetail(Number(id));
|
||||
const details = response?.data?.data;
|
||||
console.log("detail", details);
|
||||
setFiles(details?.files);
|
||||
console.log("ISI FILES:", details?.files);
|
||||
setSelectedCategory(String(details.categories[0].id));
|
||||
|
||||
setDetail(details);
|
||||
|
|
@ -268,12 +267,20 @@ export default function FormAudioDetail() {
|
|||
|
||||
if (details?.publishedForObject) {
|
||||
const publisherIds = details?.publishedForObject.map(
|
||||
(obj: any) => obj.id
|
||||
(obj: any) => obj.id,
|
||||
);
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
setSelectedTarget(String(details.category.id));
|
||||
const categoryId =
|
||||
details?.category?.id ||
|
||||
details?.categoryId ||
|
||||
details?.categories?.[0]?.id;
|
||||
|
||||
if (categoryId) {
|
||||
setSelectedTarget(String(categoryId));
|
||||
setSelectedCategory(String(categoryId));
|
||||
}
|
||||
|
||||
const filesData = details?.files || [];
|
||||
// const audioFiles = filesData.filter(
|
||||
|
|
@ -382,7 +389,7 @@ export default function FormAudioDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean
|
||||
checked: boolean,
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -416,7 +423,7 @@ export default function FormAudioDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string
|
||||
format: string,
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -448,6 +455,8 @@ export default function FormAudioDetail() {
|
|||
});
|
||||
};
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
|
|
@ -647,7 +656,7 @@ export default function FormAudioDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all"
|
||||
"all",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -664,7 +673,7 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes"
|
||||
"mabes",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "mabes", Boolean(e))
|
||||
|
|
@ -681,7 +690,7 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda"
|
||||
"polda",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "polda", Boolean(e))
|
||||
|
|
@ -699,13 +708,13 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international"
|
||||
"international",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e)
|
||||
Boolean(e),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -821,30 +830,34 @@ export default function FormAudioDetail() {
|
|||
Number(detail?.uploadedById) == Number(userId) ? (
|
||||
""
|
||||
) : ( */}
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:check" className="mr-3" /> Accept
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => actionApproval("3")}
|
||||
className="bg-orange-400 hover:bg-orange-300"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:comment-o" className="mr-3" /> Revision
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => actionApproval("4")}
|
||||
color="destructive"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:times" className="mr-3" />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:check" className="mr-3" /> Accept
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => actionApproval("3")}
|
||||
className="bg-orange-400 hover:bg-orange-300"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:comment-o" className="mr-3" /> Revision
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => actionApproval("4")}
|
||||
color="destructive"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="fa:times" className="mr-3" />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
)}
|
||||
{/* )
|
||||
) : (
|
||||
""
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
|
|||
import FileTextPreview from "../file-preview-text";
|
||||
import FileTextThumbnail from "../file-text-thumbnail";
|
||||
import { listArticleCategories } from "@/service/content";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
type Option = {
|
||||
id: string;
|
||||
|
|
@ -116,7 +117,7 @@ const ViewEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormTeksDetail() {
|
||||
|
|
@ -195,7 +196,7 @@ export default function FormTeksDetail() {
|
|||
|
||||
const handleCheckboxChange = (id: number) => {
|
||||
setSelectedPublishers((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -253,7 +254,7 @@ export default function FormTeksDetail() {
|
|||
|
||||
if (details?.publishedForObject) {
|
||||
const publisherIds = details.publishedForObject.map(
|
||||
(obj: any) => obj.id
|
||||
(obj: any) => obj.id,
|
||||
);
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
|
@ -359,7 +360,7 @@ export default function FormTeksDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean
|
||||
checked: boolean,
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -400,7 +401,7 @@ export default function FormTeksDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string
|
||||
format: string,
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -424,6 +425,8 @@ export default function FormTeksDetail() {
|
|||
});
|
||||
};
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
|
|
@ -528,10 +531,10 @@ export default function FormTeksDetail() {
|
|||
</Card>
|
||||
<div className="w-full lg:w-4/12 m-2">
|
||||
<Card className="pb-3">
|
||||
<div className="px-3 py-3">
|
||||
<Label>Creator</Label>
|
||||
<Input value={detail.createdByName} disabled />
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<Label>Creator</Label>
|
||||
<Input value={detail.createdByName} disabled />
|
||||
</div>
|
||||
{/* <div className="mt-3 px-3">
|
||||
<Label>Pratinjau Gambar Utama</Label>
|
||||
<Card className="mt-2">
|
||||
|
|
@ -647,7 +650,7 @@ export default function FormTeksDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all"
|
||||
"all",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -664,7 +667,7 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes"
|
||||
"mabes",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "mabes", Boolean(e))
|
||||
|
|
@ -681,7 +684,7 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda"
|
||||
"polda",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "polda", Boolean(e))
|
||||
|
|
@ -699,13 +702,13 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international"
|
||||
"international",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e)
|
||||
Boolean(e),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -817,10 +820,17 @@ export default function FormTeksDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
{/* {Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
|
||||
Number(detail?.uploadedById) == Number(userId) ? (
|
||||
{/* {isPending &&
|
||||
(Number(detail?.needApprovalFromLevel || 0) ===
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia === true &&
|
||||
detail?.isForwardFromNational === true)) ? (
|
||||
Number(detail?.createdById || detail?.uploadedById) ==
|
||||
Number(userId) ? (
|
||||
""
|
||||
) : ( */}
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
|
|
@ -829,6 +839,7 @@ export default function FormTeksDetail() {
|
|||
>
|
||||
<Icon icon="fa:check" className="mr-3" /> Accept
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => actionApproval("3")}
|
||||
className="bg-orange-400 hover:bg-orange-300"
|
||||
|
|
@ -836,6 +847,7 @@ export default function FormTeksDetail() {
|
|||
>
|
||||
<Icon icon="fa:comment-o" className="mr-3" /> Revision
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => actionApproval("4")}
|
||||
color="destructive"
|
||||
|
|
@ -845,7 +857,9 @@ export default function FormTeksDetail() {
|
|||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
{/* )
|
||||
</AccessGuard>
|
||||
)}
|
||||
{/* )
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import {
|
|||
getDataApprovalByMediaUpload,
|
||||
} from "@/service/curated-content/curated-content";
|
||||
import { UnitMapping } from "../unit-mapping";
|
||||
import { AccessGuard } from "@/components/access-guard";
|
||||
|
||||
const imageSchema = z.object({
|
||||
title: z.string().min(1, { message: "Judul diperlukan" }),
|
||||
|
|
@ -161,7 +162,7 @@ const ViewEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormImageDetail() {
|
||||
|
|
@ -171,10 +172,8 @@ export default function FormImageDetail() {
|
|||
const userLevelId = getCookiesDecrypt("ulie");
|
||||
const userLevelName = Cookies.get("state");
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
console.log("LALALALA", userLevelName);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { id } = useParams() as { id: string };
|
||||
console.log("IDIDIDIDI", id);
|
||||
const editor = useRef(null);
|
||||
type ImageSchema = z.infer<typeof imageSchema>;
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
|
|
@ -233,7 +232,7 @@ export default function FormImageDetail() {
|
|||
|
||||
const handleCheckboxChange = (id: number) => {
|
||||
setSelectedPublishers((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -255,7 +254,7 @@ export default function FormImageDetail() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory?.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -365,7 +364,7 @@ export default function FormImageDetail() {
|
|||
setSelectedTarget(String(mappedDetail.categoryId));
|
||||
|
||||
const fileUrls = (mappedFiles || []).map(
|
||||
(file) => file.thumbnailFileUrl || file.url || "default-image.jpg"
|
||||
(file) => file.thumbnailFileUrl || file.url || "default-image.jpg",
|
||||
);
|
||||
|
||||
setDetailThumb(fileUrls);
|
||||
|
|
@ -478,7 +477,7 @@ export default function FormImageDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean
|
||||
checked: boolean,
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -519,7 +518,7 @@ export default function FormImageDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string
|
||||
format: string,
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -566,6 +565,15 @@ export default function FormImageDetail() {
|
|||
console.log("portrai", portraitMap);
|
||||
}, [portraitMap]);
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
const isApproved = Number(detail?.statusId) === 2;
|
||||
const isRejected = Number(detail?.statusId) === 4;
|
||||
|
||||
const isCreator =
|
||||
Number(detail?.createdById || detail?.uploadedById) === Number(userId);
|
||||
|
||||
const hasApproval = approval != null;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
|
|
@ -615,14 +623,14 @@ export default function FormImageDetail() {
|
|||
!categories?.find(
|
||||
(cat) =>
|
||||
String(cat.id) ===
|
||||
String(detail.categoryId || detail?.category?.id)
|
||||
String(detail.categoryId || detail?.category?.id),
|
||||
) && (
|
||||
<SelectItem
|
||||
key={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
detail.categoryId || detail?.category?.id,
|
||||
)}
|
||||
value={String(
|
||||
detail.categoryId || detail?.category?.id
|
||||
detail.categoryId || detail?.category?.id,
|
||||
)}
|
||||
>
|
||||
{detail.categoryName || detail?.category?.name}
|
||||
|
|
@ -915,7 +923,7 @@ export default function FormImageDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all"
|
||||
"all",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -932,13 +940,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes"
|
||||
"mabes",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"mabes",
|
||||
Boolean(e)
|
||||
Boolean(e),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -953,13 +961,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda"
|
||||
"polda",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"polda",
|
||||
Boolean(e)
|
||||
Boolean(e),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -984,13 +992,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international"
|
||||
"international",
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e)
|
||||
Boolean(e),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -1111,15 +1119,22 @@ export default function FormImageDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
{Number(detail?.needApprovalFromLevel || 0) ==
|
||||
{/* {Number(detail?.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia == true &&
|
||||
detail?.isForwardFromNational == true &&
|
||||
Number(detail?.statusId) == 1) ? (
|
||||
Number(detail?.statusId) == 1) ? ( */}
|
||||
{/* {isPending &&
|
||||
(Number(detail?.needApprovalFromLevel || 0) ===
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia === true &&
|
||||
detail?.isForwardFromNational === true)) ? (
|
||||
Number(detail?.createdById || detail?.uploadedById) ==
|
||||
Number(userId) ? (
|
||||
""
|
||||
) : (
|
||||
) : ( */}
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
|
|
@ -1145,10 +1160,12 @@ export default function FormImageDetail() {
|
|||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
</AccessGuard>
|
||||
)}
|
||||
{/* )
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export default function SignUp() {
|
|||
const [whatsapp, setWhatsapp] = useState("");
|
||||
const [namaTenant, setNamaTenant] = useState("");
|
||||
const [tenantPassword, setTenantPassword] = useState("");
|
||||
const [tenantUsername, setTenantUsername] = useState("");
|
||||
const [confirmTenantPassword, setConfirmTenantPassword] = useState("");
|
||||
const [firstNameKontributor, setFirstNameKontributor] = useState("");
|
||||
const [lastNameKontributor, setLastNameKontributor] = useState("");
|
||||
|
|
@ -119,7 +120,11 @@ export default function SignUp() {
|
|||
if (value && nextInput) nextInput.focus();
|
||||
};
|
||||
|
||||
// Form validation functions
|
||||
const validateName = (value: string) => {
|
||||
const nameRegex = /^[A-Za-z\s]+$/;
|
||||
return nameRegex.test(value.trim());
|
||||
};
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
|
|
@ -134,6 +139,11 @@ export default function SignUp() {
|
|||
return password.length >= 8;
|
||||
};
|
||||
|
||||
const validateUsername = (username: string) => {
|
||||
const usernameRegex = /^[a-z0-9_-]+$/;
|
||||
return usernameRegex.test(username);
|
||||
};
|
||||
|
||||
const handleCreateUserKontributor = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -141,7 +151,7 @@ export default function SignUp() {
|
|||
MySwal.fire(
|
||||
"Peringatan",
|
||||
"Nama depan dan belakang wajib diisi",
|
||||
"warning"
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -260,12 +270,29 @@ export default function SignUp() {
|
|||
const validateTenantForm = () => {
|
||||
const errors: any = {};
|
||||
|
||||
// if (!firstName.trim()) {
|
||||
// errors.firstName = "First name is required";
|
||||
// }
|
||||
if (!firstName.trim()) {
|
||||
errors.firstName = "First name is required";
|
||||
errors.firstName = "First name wajib diisi";
|
||||
} else if (!validateName(firstName)) {
|
||||
errors.firstName = "First name hanya boleh huruf dan spasi";
|
||||
}
|
||||
|
||||
// if (!lastName.trim()) {
|
||||
// errors.lastName = "Last name is required";
|
||||
// }
|
||||
if (!lastName.trim()) {
|
||||
errors.lastName = "Last name is required";
|
||||
errors.lastName = "Last name wajib diisi";
|
||||
} else if (!validateName(lastName)) {
|
||||
errors.lastName = "Last name hanya boleh huruf dan spasi";
|
||||
}
|
||||
|
||||
if (!tenantUsername.trim()) {
|
||||
errors.tenantUsername = "Username wajib diisi";
|
||||
} else if (!validateUsername(tenantUsername)) {
|
||||
errors.tenantUsername =
|
||||
"Username hanya boleh huruf kecil, angka, tanpa spasi";
|
||||
}
|
||||
|
||||
if (!email.trim()) {
|
||||
|
|
@ -303,9 +330,7 @@ export default function SignUp() {
|
|||
const handleTenantRegistration = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateTenantForm()) {
|
||||
return;
|
||||
}
|
||||
if (!validateTenantForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setFormErrors({});
|
||||
|
|
@ -314,43 +339,75 @@ export default function SignUp() {
|
|||
const registrationData = {
|
||||
adminUser: {
|
||||
address: "Jakarta",
|
||||
email: email,
|
||||
fullname: `${firstName} ${lastName}`,
|
||||
email: email.trim(),
|
||||
fullname: `${firstName.trim()} ${lastName.trim()}`,
|
||||
password: tenantPassword,
|
||||
phoneNumber: whatsapp,
|
||||
username: `${firstName}-${lastName}`,
|
||||
phoneNumber: whatsapp.trim(),
|
||||
username: tenantUsername.trim(),
|
||||
},
|
||||
client: {
|
||||
clientType: "sub_client",
|
||||
name: namaTenant,
|
||||
name: namaTenant.trim(),
|
||||
parentClientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await registerTenant(registrationData);
|
||||
|
||||
if (response.error) {
|
||||
console.log("📦 registerTenant response:", response);
|
||||
|
||||
/**
|
||||
* ❗ KUNCI UTAMA
|
||||
* Backend gagal → error === true
|
||||
*/
|
||||
if (response.error === true) {
|
||||
const backendMessage =
|
||||
typeof response.message === "string"
|
||||
? response.message
|
||||
: JSON.stringify(response.message);
|
||||
|
||||
// 🔴 Duplicate tenant / slug
|
||||
if (
|
||||
backendMessage.includes("clients_slug_key") ||
|
||||
backendMessage.toLowerCase().includes("duplicate")
|
||||
) {
|
||||
MySwal.fire({
|
||||
title: "Tenant Sudah Terdaftar",
|
||||
text: "Nama tenant sudah digunakan. Silakan gunakan nama tenant lain.",
|
||||
icon: "warning",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔴 General error
|
||||
MySwal.fire({
|
||||
title: "Registration Failed",
|
||||
text: response.message || "An error occurred during registration",
|
||||
title: "Registrasi Gagal",
|
||||
text: backendMessage || "Tenant gagal dibuat",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
} else {
|
||||
MySwal.fire({
|
||||
title: "Registration Successful",
|
||||
text: "Your tenant account has been created successfully!",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
}).then(() => {
|
||||
router.push("/auth");
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
|
||||
/**
|
||||
* ✅ SUCCESS
|
||||
* HANYA jika error === false
|
||||
*/
|
||||
await MySwal.fire({
|
||||
title: "Registrasi Berhasil 🎉",
|
||||
text: "Akun tenant berhasil dibuat. Silakan login.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
||||
router.push("/auth");
|
||||
} catch (err) {
|
||||
console.error("❌ Tenant registration error:", err);
|
||||
|
||||
MySwal.fire({
|
||||
title: "Registration Failed",
|
||||
text: "An unexpected error occurred. Please try again.",
|
||||
title: "Terjadi Kesalahan",
|
||||
text: "Terjadi kesalahan server. Silakan coba lagi.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
|
@ -359,6 +416,66 @@ export default function SignUp() {
|
|||
}
|
||||
};
|
||||
|
||||
// const handleTenantRegistration = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
|
||||
// if (!validateTenantForm()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setIsLoading(true);
|
||||
// setFormErrors({});
|
||||
|
||||
// try {
|
||||
// const registrationData = {
|
||||
// adminUser: {
|
||||
// address: "Jakarta",
|
||||
// email: email,
|
||||
// fullname: `${firstName} ${lastName}`,
|
||||
// password: tenantPassword,
|
||||
// phoneNumber: whatsapp,
|
||||
// // username: `${firstName}-${lastName}`,
|
||||
// username: tenantUsername,
|
||||
// },
|
||||
// client: {
|
||||
// clientType: "sub_client",
|
||||
// name: namaTenant,
|
||||
// parentClientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
|
||||
// },
|
||||
// };
|
||||
|
||||
// const response = await registerTenant(registrationData);
|
||||
|
||||
// if (response.error) {
|
||||
// MySwal.fire({
|
||||
// title: "Registration Failed",
|
||||
// text: response.message || "An error occurred during registration",
|
||||
// icon: "error",
|
||||
// confirmButtonText: "OK",
|
||||
// });
|
||||
// } else {
|
||||
// MySwal.fire({
|
||||
// title: "Registration Successful",
|
||||
// text: "Your tenant account has been created successfully!",
|
||||
// icon: "success",
|
||||
// confirmButtonText: "OK",
|
||||
// }).then(() => {
|
||||
// router.push("/auth");
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Registration error:", error);
|
||||
// MySwal.fire({
|
||||
// title: "Registration Failed",
|
||||
// text: "An unexpected error occurred. Please try again.",
|
||||
// icon: "error",
|
||||
// confirmButtonText: "OK",
|
||||
// });
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// Generate username otomatis dari nama lengkap
|
||||
React.useEffect(() => {
|
||||
if (fullname.trim()) {
|
||||
|
|
@ -685,6 +802,9 @@ export default function SignUp() {
|
|||
formErrors.firstName ? "border-red-500" : ""
|
||||
}
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 mt-1">
|
||||
Hanya huruf dan spasi.
|
||||
</p>
|
||||
{formErrors.firstName && (
|
||||
<p className="text-red-500 text-xs mt-1">
|
||||
{formErrors.firstName}
|
||||
|
|
@ -710,6 +830,32 @@ export default function SignUp() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Username"
|
||||
value={tenantUsername}
|
||||
onChange={(e) =>
|
||||
setTenantUsername(e.target.value.toLowerCase())
|
||||
}
|
||||
className={
|
||||
formErrors.tenantUsername ? "border-red-500" : ""
|
||||
}
|
||||
/>
|
||||
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Username harus huruf kecil, tanpa spasi. Contoh:{" "}
|
||||
<b>netidhub-admin</b>
|
||||
</p>
|
||||
|
||||
{formErrors.tenantUsername && (
|
||||
<p className="text-red-500 text-xs mt-1">
|
||||
{formErrors.tenantUsername}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="email"
|
||||
|
|
|
|||
|
|
@ -8,29 +8,42 @@ 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { errorAutoClose, loading, successAutoClose } from "@/lib/swal";
|
||||
import { close } from "@/config/swal";
|
||||
import {
|
||||
createUser,
|
||||
updateUser,
|
||||
getUserDetail,
|
||||
import {
|
||||
createUser,
|
||||
updateUser,
|
||||
getUserDetail,
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest
|
||||
UpdateUserRequest,
|
||||
} from "@/service/management-user/management-user";
|
||||
import { getInfoProfile } from "@/service/auth";
|
||||
import { getUserLevels } from "@/service/approval-workflows";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
address: z.string().trim().min(1, { message: "Address wajib diisi" }),
|
||||
clientId: z.string().trim().min(1, { message: "Client ID wajib diisi" }),
|
||||
dateOfBirth: z.string().trim().min(1, { message: "Date of Birth wajib diisi" }),
|
||||
dateOfBirth: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Date of Birth wajib diisi" }),
|
||||
email: z.string().email({ message: "Email tidak valid" }),
|
||||
fullname: z.string().trim().min(1, { message: "Full Name wajib diisi" }),
|
||||
password: z.string().min(6, { message: "Password minimal 6 karakter" }),
|
||||
phoneNumber: z.string().trim().min(1, { message: "Phone Number wajib diisi" }),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Phone Number wajib diisi" }),
|
||||
userLevelId: z.number({ invalid_type_error: "User Level harus dipilih" }),
|
||||
username: z.string().trim().min(1, { message: "Username wajib diisi" }),
|
||||
});
|
||||
|
|
@ -38,11 +51,20 @@ const createUserSchema = z.object({
|
|||
const editUserSchema = z.object({
|
||||
address: z.string().trim().min(1, { message: "Address wajib diisi" }),
|
||||
clientId: z.string().trim().min(1, { message: "Client ID wajib diisi" }),
|
||||
dateOfBirth: z.string().trim().min(1, { message: "Date of Birth wajib diisi" }),
|
||||
dateOfBirth: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Date of Birth wajib diisi" }),
|
||||
email: z.string().email({ message: "Email tidak valid" }),
|
||||
fullname: z.string().trim().min(1, { message: "Full Name wajib diisi" }),
|
||||
password: z.string().min(6, { message: "Password minimal 6 karakter" }).optional(),
|
||||
phoneNumber: z.string().trim().min(1, { message: "Phone Number wajib diisi" }),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, { message: "Password minimal 6 karakter" })
|
||||
.optional(),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Phone Number wajib diisi" }),
|
||||
userLevelId: z.number({ invalid_type_error: "User Level harus dipilih" }),
|
||||
username: z.string().trim().min(1, { message: "Username wajib diisi" }),
|
||||
});
|
||||
|
|
@ -67,8 +89,11 @@ export default function UserForm({
|
|||
}: UserFormProps) {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [userLevels, setUserLevels] = useState<{id: number; name: string}[]>([]);
|
||||
const [userLevels, setUserLevels] = useState<{ id: number; name: string }[]>(
|
||||
[],
|
||||
);
|
||||
const [currentClientId, setCurrentClientId] = useState<string>("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
|
@ -76,7 +101,9 @@ export default function UserForm({
|
|||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<UserSchema>({
|
||||
resolver: zodResolver(mode === "create" ? createUserSchema : editUserSchema),
|
||||
resolver: zodResolver(
|
||||
mode === "create" ? createUserSchema : editUserSchema,
|
||||
),
|
||||
defaultValues: {
|
||||
address: "",
|
||||
clientId: "",
|
||||
|
|
@ -106,7 +133,8 @@ export default function UserForm({
|
|||
// Get clientId from current user info
|
||||
if (!userInfoResponse?.error && userInfoResponse?.data?.data) {
|
||||
const userInfo = userInfoResponse.data.data;
|
||||
const clientId = userInfo.instituteId || "78356d32-52fa-4dfc-b836-6cebf4e3eead"; // fallback to default
|
||||
const clientId =
|
||||
userInfo.instituteId || "78356d32-52fa-4dfc-b836-6cebf4e3eead"; // fallback to default
|
||||
setCurrentClientId(clientId);
|
||||
setValue("clientId", clientId);
|
||||
} else {
|
||||
|
|
@ -132,7 +160,10 @@ export default function UserForm({
|
|||
setValue("username", user.username || "");
|
||||
// Don't set password for edit mode
|
||||
} else {
|
||||
console.error("Gagal mengambil detail user:", userResponse?.message || "Unknown error");
|
||||
console.error(
|
||||
"Gagal mengambil detail user:",
|
||||
userResponse?.message || "Unknown error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading user detail:", error);
|
||||
|
|
@ -161,7 +192,7 @@ export default function UserForm({
|
|||
password: data.password || "",
|
||||
phoneNumber: data.phoneNumber,
|
||||
userLevelId: data.userLevelId,
|
||||
userRoleId: 3, // Hardcoded as per requirement
|
||||
userRoleId: 3,
|
||||
username: data.username,
|
||||
};
|
||||
|
||||
|
|
@ -177,18 +208,25 @@ export default function UserForm({
|
|||
close();
|
||||
|
||||
if (response?.error) {
|
||||
errorAutoClose(response.message || `Gagal ${mode === "edit" ? "memperbarui" : "menyimpan"} data.`);
|
||||
errorAutoClose(
|
||||
response.message ||
|
||||
`Gagal ${mode === "edit" ? "memperbarui" : "menyimpan"} data.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
successAutoClose(`Data berhasil ${mode === "edit" ? "diperbarui" : "disimpan"}.`);
|
||||
successAutoClose(
|
||||
`Data berhasil ${mode === "edit" ? "diperbarui" : "disimpan"}.`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
if (onSuccess) onSuccess();
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
close();
|
||||
errorAutoClose(`Terjadi kesalahan saat ${mode === "edit" ? "memperbarui" : "menyimpan"} data.`);
|
||||
errorAutoClose(
|
||||
`Terjadi kesalahan saat ${mode === "edit" ? "memperbarui" : "menyimpan"} data.`,
|
||||
);
|
||||
console.error("User operation error:", err);
|
||||
}
|
||||
};
|
||||
|
|
@ -261,6 +299,41 @@ export default function UserForm({
|
|||
|
||||
{/* Password - Only show for create mode */}
|
||||
{mode === "create" && (
|
||||
<div>
|
||||
<Label>Password</Label>
|
||||
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Masukkan password"
|
||||
className="pr-10"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* {mode === "create" && (
|
||||
<div>
|
||||
<Label>Password</Label>
|
||||
<Controller
|
||||
|
|
@ -274,7 +347,7 @@ export default function UserForm({
|
|||
<p className="text-red-500 text-sm">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* Phone Number */}
|
||||
<div>
|
||||
|
|
@ -287,7 +360,9 @@ export default function UserForm({
|
|||
)}
|
||||
/>
|
||||
{errors.phoneNumber && (
|
||||
<p className="text-red-500 text-sm">{errors.phoneNumber.message}</p>
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.phoneNumber.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -312,17 +387,17 @@ export default function UserForm({
|
|||
<Controller
|
||||
control={control}
|
||||
name="dateOfBirth"
|
||||
render={({ field }) => (
|
||||
<Input {...field} type="date" />
|
||||
)}
|
||||
render={({ field }) => <Input {...field} type="date" />}
|
||||
/>
|
||||
{errors.dateOfBirth && (
|
||||
<p className="text-red-500 text-sm">{errors.dateOfBirth.message}</p>
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.dateOfBirth.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Client ID */}
|
||||
<div>
|
||||
{/* <div>
|
||||
<Label>Client ID</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -342,7 +417,7 @@ export default function UserForm({
|
|||
<p className="text-xs text-gray-500 mt-1">
|
||||
Client ID diambil dari profil pengguna yang sedang login
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* User Level */}
|
||||
<div>
|
||||
|
|
@ -351,7 +426,10 @@ export default function UserForm({
|
|||
control={control}
|
||||
name="userLevelId"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={(value) => field.onChange(Number(value))} value={field.value?.toString() || ""}>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
value={field.value?.toString() || ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Pilih user level" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -372,16 +450,16 @@ export default function UserForm({
|
|||
)}
|
||||
/>
|
||||
{errors.userLevelId && (
|
||||
<p className="text-red-500 text-sm">{errors.userLevelId.message}</p>
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.userLevelId.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button type="submit">
|
||||
{mode === "edit" ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button type="submit">{mode === "edit" ? "Update" : "Create"}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => onCancel?.()}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ const UserInternalTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white dark:bg-slate-200">
|
||||
<div className="flex justify-between py-3">
|
||||
<Input
|
||||
type="text"
|
||||
|
|
@ -396,7 +396,7 @@ const UserInternalTable = () => {
|
|||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,28 +49,59 @@ export async function getUserLevelMenuActionAccesses(params?: {
|
|||
limit?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.userLevelId) queryParams.append("user_level_id", params.userLevelId.toString());
|
||||
|
||||
if (params?.userLevelId)
|
||||
queryParams.append("user_level_id", params.userLevelId.toString());
|
||||
if (params?.menuId) queryParams.append("menu_id", params.menuId.toString());
|
||||
if (params?.actionCode) queryParams.append("action_code", params.actionCode);
|
||||
if (params?.canAccess !== undefined) queryParams.append("can_access", params.canAccess.toString());
|
||||
if (params?.canAccess !== undefined)
|
||||
queryParams.append("can_access", params.canAccess.toString());
|
||||
if (params?.page) queryParams.append("page", params.page.toString());
|
||||
if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
const url = `user-level-menu-action-accesses${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
const url = `user-level-menu-action-accesses${
|
||||
queryParams.toString() ? `?${queryParams.toString()}` : ""
|
||||
}`;
|
||||
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
// export async function getUserLevelMenuActionAccesses(params?: {
|
||||
// userLevelId?: number;
|
||||
// menuId?: number;
|
||||
// actionCode?: string;
|
||||
// canAccess?: boolean;
|
||||
// page?: number;
|
||||
// limit?: number;
|
||||
// }) {
|
||||
// const queryParams = new URLSearchParams();
|
||||
// if (params?.userLevelId) queryParams.append("user_level_id", params.userLevelId.toString());
|
||||
// if (params?.menuId) queryParams.append("menu_id", params.menuId.toString());
|
||||
// if (params?.actionCode) queryParams.append("action_code", params.actionCode);
|
||||
// if (params?.canAccess !== undefined) queryParams.append("can_access", params.canAccess.toString());
|
||||
// if (params?.page) queryParams.append("page", params.page.toString());
|
||||
// if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
// const url = `user-level-menu-action-accesses${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
// return httpGetInterceptor(url);
|
||||
// }
|
||||
|
||||
export async function getUserLevelMenuActionAccessById(id: number) {
|
||||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelId(userLevelId: number) {
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelId(
|
||||
userLevelId: number,
|
||||
) {
|
||||
const url = `user-level-menu-action-accesses/user-level/${userLevelId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(userLevelId: number, menuId: number) {
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(
|
||||
userLevelId: number,
|
||||
menuId: number,
|
||||
) {
|
||||
const url = `user-level-menu-action-accesses/user-level/${userLevelId}/menu/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
|
@ -80,22 +111,33 @@ export async function getUserLevelMenuActionAccessesByMenuId(menuId: number) {
|
|||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function checkUserLevelMenuActionAccess(userLevelId: number, menuId: number, actionCode: string) {
|
||||
export async function checkUserLevelMenuActionAccess(
|
||||
userLevelId: number,
|
||||
menuId: number,
|
||||
actionCode: string,
|
||||
) {
|
||||
const url = `user-level-menu-action-accesses/check/${userLevelId}/${menuId}/${actionCode}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuActionAccess(data: UserLevelMenuActionAccessCreateRequest) {
|
||||
export async function createUserLevelMenuActionAccess(
|
||||
data: UserLevelMenuActionAccessCreateRequest,
|
||||
) {
|
||||
const url = "user-level-menu-action-accesses";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuActionAccessesBatch(data: UserLevelMenuActionAccessBatchCreateRequest) {
|
||||
export async function createUserLevelMenuActionAccessesBatch(
|
||||
data: UserLevelMenuActionAccessBatchCreateRequest,
|
||||
) {
|
||||
const url = "user-level-menu-action-accesses/batch";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateUserLevelMenuActionAccess(id: number, data: UserLevelMenuActionAccessUpdateRequest) {
|
||||
export async function updateUserLevelMenuActionAccess(
|
||||
id: number,
|
||||
data: UserLevelMenuActionAccessUpdateRequest,
|
||||
) {
|
||||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
|
@ -104,4 +146,3 @@ export async function deleteUserLevelMenuActionAccess(id: number) {
|
|||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { httpGetInterceptor } from "./http-config/http-interceptor-service";
|
||||
|
||||
export function getUserInfo() {
|
||||
return httpGetInterceptor("users/info");
|
||||
}
|
||||
Loading…
Reference in New Issue