Compare commits
No commits in common. "main" and "chore/upgrade-next-react2" have entirely different histories.
main
...
chore/upgr
44
.drone.yml
44
.drone.yml
|
|
@ -1,44 +0,0 @@
|
|||
kind: pipeline
|
||||
type: ssh
|
||||
name: kontenhumas-fe-build-deploy
|
||||
|
||||
server:
|
||||
host:
|
||||
from_secret: ssh_host
|
||||
user:
|
||||
from_secret: ssh_user
|
||||
ssh_key:
|
||||
from_secret: ssh_key
|
||||
|
||||
steps:
|
||||
- name: prepare repo
|
||||
when:
|
||||
branch:
|
||||
- dev-1
|
||||
- main
|
||||
commands:
|
||||
- rm -rf /opt/build/kontenhumas-fe
|
||||
- mkdir -p /opt/build
|
||||
- cd /opt/build
|
||||
- git clone http://38.47.180.165:3000/kontenhumas/kontenhumas-fe.git
|
||||
|
||||
- name: build image
|
||||
when:
|
||||
branch:
|
||||
- dev-1
|
||||
- main
|
||||
commands:
|
||||
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
|
||||
- cd /opt/build/kontenhumas-fe
|
||||
- docker build -t 38.47.180.165:3000/kontenhumas/kontenhumas-fe:$DRONE_BRANCH .
|
||||
- docker push 38.47.180.165:3000/kontenhumas/kontenhumas-fe:$DRONE_BRANCH
|
||||
|
||||
- name: deploy
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
commands:
|
||||
- docker pull 38.47.180.165:3000/kontenhumas/kontenhumas-fe:$DRONE_BRANCH
|
||||
- docker stop new-netidhub || true
|
||||
- docker rm new-netidhub || true
|
||||
- docker run -dt -p 4242:3000 --restart always --name new-netidhub 38.47.180.165:3000/kontenhumas/kontenhumas-fe:$DRONE_BRANCH
|
||||
1
.env
1
.env
|
|
@ -1,2 +1 @@
|
|||
NETIDHUB_CLIENT_KEY=b1ce6602-07ad-46c2-85eb-0cd6decfefa3
|
||||
NEXT_PUBLIC_API_URL=https://kontenhumas.com/api
|
||||
|
|
@ -36,4 +36,3 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.env.local
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ build-dev:
|
|||
image: docker:24.0.5
|
||||
services:
|
||||
- name: docker:24.0.5-dind
|
||||
command: ["--tls=false", "--insecure-registry=38.47.185.86:8900"]
|
||||
command: ["--tls=false", "--insecure-registry=103.82.242.92:8900"]
|
||||
variables:
|
||||
DOCKER_HOST: "tcp://docker:2375"
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
script:
|
||||
- docker logout
|
||||
- echo "$DEPLOY_TOKEN" | docker login -u "$DEPLOY_USERNAME" --password-stdin http://38.47.185.86:8900
|
||||
- echo "$DEPLOY_TOKEN" | docker login -u "$DEPLOY_USERNAME" --password-stdin http://103.82.242.92:8900
|
||||
- docker build -t new-netidhub-dev .
|
||||
- docker tag new-netidhub-dev 38.47.185.86:8900/medols/new-netidhub:dev
|
||||
- docker push 38.47.185.86:8900/medols/new-netidhub:dev
|
||||
- docker tag new-netidhub-dev 103.82.242.92:8900/medols/new-netidhub:dev
|
||||
- docker push 103.82.242.92:8900/medols/new-netidhub:dev
|
||||
|
||||
auto-deploy:
|
||||
stage: deploy
|
||||
|
|
|
|||
20
Dockerfile
20
Dockerfile
|
|
@ -4,16 +4,6 @@ FROM node:23.5.0-alpine
|
|||
# Mengatur port
|
||||
ENV PORT 3000
|
||||
|
||||
# Build arguments untuk environment variables (build-time)
|
||||
# Bisa di-override saat docker build dengan --build-arg
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ARG NEXT_PUBLIC_SITE_URL
|
||||
|
||||
# Set sebagai environment variables untuk build
|
||||
# Next.js membaca NEXT_PUBLIC_* variables saat BUILD TIME
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||||
|
||||
# Install pnpm secara global
|
||||
RUN npm install -g pnpm
|
||||
|
||||
|
|
@ -26,19 +16,17 @@ COPY package.json ./
|
|||
# Menyalin direktori ckeditor5 jika diperlukan
|
||||
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||
|
||||
# Menyalin env
|
||||
COPY .env .env
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
# RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Menyalin source code aplikasi (termasuk .env jika ada)
|
||||
# PENTING: Next.js akan membaca file .env otomatis jika ada
|
||||
# Tapi jika ARG di-set, ARG akan override nilai dari .env
|
||||
# Menyalin source code aplikasi
|
||||
COPY . .
|
||||
|
||||
# Build aplikasi
|
||||
# Next.js membaca NEXT_PUBLIC_* dari:
|
||||
# 1. Environment variables (ENV) - prioritas tertinggi
|
||||
# 2. File .env - jika ENV tidak di-set
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
|
||||
|
||||
# Expose port untuk server
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ import {
|
|||
checkRolePlacementsAvailability,
|
||||
getListCompetencies,
|
||||
getListExperiences,
|
||||
saveUserInternal,
|
||||
saveUserRolePlacements,
|
||||
} from "@/service/management-user/management-user";
|
||||
import { error, loading } from "@/config/swal";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { saveUserInternal } from "@/service/service/management-user/management-user";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ import {
|
|||
getListCompetencies,
|
||||
getListExperiences,
|
||||
getUserById,
|
||||
saveUserInternal,
|
||||
saveUserRolePlacements,
|
||||
} from "@/service/management-user/management-user";
|
||||
import { loading } from "@/config/swal";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { saveUserInternal } from "@/service/service/management-user/management-user";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const TableCategories = () => {
|
|||
{
|
||||
accessorKey: "statusId",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (row.original.isActive ? "Active" : "Draft"),
|
||||
cell: ({ row }) => (row.original.isPublish ? "Publish" : "Draft"),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
|
|
@ -187,7 +187,7 @@ const TableCategories = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-default-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="w-full overflow-x-auto bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="flex justify-between items-center px-4 py-2">
|
||||
<h2 className="text-lg font-semibold">Daftar Kategori</h2>
|
||||
|
||||
|
|
|
|||
|
|
@ -127,12 +127,12 @@ const ReactTableImagePage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-default-50">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white dark:bg-default-50 rounded-t-lg">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -140,7 +140,7 @@ const ReactTableImagePage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Categories Management
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
|
|
@ -294,7 +294,7 @@ const ReactTableImagePage = () => {
|
|||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50 dark:bg-default-50">
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<TableCategories />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const page = () => {
|
|||
return (
|
||||
<div className="">
|
||||
<SiteBreadcrumb />
|
||||
<div className="bg-slate-100 dark:bg-default-50">
|
||||
<div className="bg-slate-100">
|
||||
<CategoriesUpdateForm />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const AudioTabs = () => {
|
|||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{/* <TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="submitted"
|
||||
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
|
||||
|
|
@ -31,20 +31,20 @@ const AudioTabs = () => {
|
|||
Waiting Approval
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList> */}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
<div className="bg-white dark:bg-default-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<TableAudio />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={4} />
|
||||
</div>
|
||||
</TabsContent> */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ 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);
|
||||
|
|
@ -138,119 +137,57 @@ const useTableColumns = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
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 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 (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// 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",
|
||||
|
|
@ -304,7 +241,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]);
|
||||
|
|
@ -321,15 +258,12 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<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/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>
|
||||
{/* <Link
|
||||
href={`/admin/content/audio/update/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -338,31 +272,22 @@ const useTableColumns = () => {
|
|||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link> */}
|
||||
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
|
||||
<AccessGuard action="edit">
|
||||
{(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && (
|
||||
<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>
|
||||
</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>
|
||||
|
||||
)}
|
||||
<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>
|
||||
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import FormAudio from "@/components/form/content/audio/audio-form";
|
|||
const AudioCreatePage = async () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormAudio />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const AudioDetailPage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormAudioDetail />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,15 @@ 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 dark:bg-default-50">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white dark:bg-default-50 rounded-t-lg">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -25,31 +21,22 @@ const ReactTableAudioPage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
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>
|
||||
<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 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>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50 dark:bg-default-50">
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<AudioTabs />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ 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);
|
||||
|
|
@ -67,10 +66,7 @@ const useTableColumns = () => {
|
|||
accessorKey: "createdAt",
|
||||
header: "Upload Date",
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as
|
||||
| string
|
||||
| number
|
||||
| undefined;
|
||||
const createdAt = row.getValue("createdAt") as string | number | undefined;
|
||||
const formattedDate =
|
||||
createdAt && !isNaN(new Date(createdAt).getTime())
|
||||
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
|
||||
|
|
@ -102,8 +98,7 @@ const useTableColumns = () => {
|
|||
cell: ({ row }) => {
|
||||
const isPublish = row.original.isPublish;
|
||||
const isPublishOnPolda = row.original.isPublishOnPolda;
|
||||
const creatorGroupParentLevelId =
|
||||
row.original.creatorGroupParentLevelId;
|
||||
const creatorGroupParentLevelId = row.original.creatorGroupParentLevelId;
|
||||
|
||||
let displayText = "-";
|
||||
if (isPublish && !isPublishOnPolda) {
|
||||
|
|
@ -129,100 +124,40 @@ 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 userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
// const isCreator = creatorGroupLevelId === Number(userLevelId);
|
||||
|
||||
// const isWaitingForReview = statusId === 2 && !userHasReviewed && !isCreator;
|
||||
// const isApprovalNeeded = statusId === 1 && needApprovalFromLevel === Number(userLevelId);
|
||||
|
||||
// const label =
|
||||
// isWaitingForReview || isApprovalNeeded
|
||||
// ? "Menunggu Review"
|
||||
// : statusId === 2
|
||||
// ? "Diterima"
|
||||
// : row.original?.statusName;
|
||||
|
||||
// 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",
|
||||
// };
|
||||
|
||||
// const statusStyles = colors[label] || colors.default;
|
||||
|
||||
// return (
|
||||
// <Badge className={cn("rounded-full px-5 w-full whitespace-nowrap", statusStyles)}>
|
||||
// {label}
|
||||
// </Badge>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
statusId,
|
||||
statusName,
|
||||
isPublish,
|
||||
reviewedAtLevel = "",
|
||||
creatorGroupLevelId,
|
||||
needApprovalFromLevel,
|
||||
} = row.original;
|
||||
|
||||
const userLevelId = Number(getCookiesDecrypt("ulie"));
|
||||
const statusId = Number(row.original?.statusId);
|
||||
const reviewedAtLevel = row.original?.reviewedAtLevel || "";
|
||||
const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
|
||||
const needApprovalFromLevel = Number(row.original?.needApprovalFromLevel);
|
||||
|
||||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
const isCreator = creatorGroupLevelId === 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 isWaitingForReview = statusId === 2 && !userHasReviewed && !isCreator;
|
||||
const isApprovalNeeded = statusId === 1 && needApprovalFromLevel === Number(userLevelId);
|
||||
|
||||
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 label =
|
||||
isWaitingForReview || isApprovalNeeded
|
||||
? "Menunggu Review"
|
||||
: statusId === 2
|
||||
? "Diterima"
|
||||
: row.original?.statusName;
|
||||
|
||||
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",
|
||||
Diterima: "bg-green-100 text-green-600",
|
||||
default: "bg-red-200 text-red-600",
|
||||
};
|
||||
|
||||
const statusStyles = colors[label] || colors.default;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Badge className={cn("rounded-full px-5 w-full whitespace-nowrap", statusStyles)}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -280,9 +215,7 @@ const useTableColumns = () => {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (userLevelId !== undefined && roleId !== undefined) {
|
||||
setIsMabesApprover(
|
||||
Number(userLevelId) === 216 && Number(roleId) === 3,
|
||||
);
|
||||
setIsMabesApprover(Number(userLevelId) === 216 && Number(roleId) === 3);
|
||||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
||||
|
|
@ -291,45 +224,37 @@ const useTableColumns = () => {
|
|||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent cursor-pointer"
|
||||
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">
|
||||
<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>
|
||||
<Link
|
||||
href={`/admin/content/image/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) && ( */}
|
||||
<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">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</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 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</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" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import TableImage from "./table-image";
|
|||
import PendingApprovalTable from "./pending-approval-table";
|
||||
|
||||
const ImageTabs = () => {
|
||||
const [activeTab, setActiveTab] = React.useState("submitted");
|
||||
const [activeTab, setActiveTab] = React.useState("pending");
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
{/* <div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="pending"
|
||||
|
|
@ -33,18 +33,18 @@ const ImageTabs = () => {
|
|||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
<div className="bg-white dark:bg-default-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<TableImage />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={1} />
|
||||
</div>
|
||||
</TabsContent> */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ const usePendingApprovalColumns = () => {
|
|||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent cursor-pointer"
|
||||
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" />
|
||||
|
|
@ -187,7 +187,7 @@ const usePendingApprovalColumns = () => {
|
|||
href={`/admin/content/image/detail/${row.original.id}`}
|
||||
className="hover:text-black"
|
||||
>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none cursor-pointer hover:bg-slate-200">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const TableImage = () => {
|
|||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
const [showData, setShowData] = React.useState("50");
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
|
|
@ -235,7 +235,7 @@ const TableImage = () => {
|
|||
totalPage: Number(showData),
|
||||
title: search || undefined,
|
||||
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
|
||||
typeId: 1,
|
||||
typeId: 1, // image content typeoriginalRows
|
||||
statusId: statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
|
||||
startDate: formattedStartDate || undefined,
|
||||
endDate: formattedEndDate || undefined,
|
||||
|
|
@ -290,9 +290,9 @@ const TableImage = () => {
|
|||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
|
||||
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2 border border-slate-200">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 " />
|
||||
<input
|
||||
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Judul..."
|
||||
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const ImageCreatePage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormImage />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const ImageDetailPage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormImageDetail />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,15 @@ 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 dark:bg-default-50">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white dark:bg-default-50 rounded-t-lg">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -25,37 +21,28 @@ const ReactTableImagePage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
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>
|
||||
<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"}>
|
||||
<div className="flex-none">
|
||||
<Link href={"/admin/content/image/create"}>
|
||||
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<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>
|
||||
</AccessGuard>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50 dark:bg-default-50">
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<ImageTabs />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ 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);
|
||||
|
|
@ -143,59 +142,50 @@ const useTableColumns = () => {
|
|||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
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 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 (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -252,7 +242,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]);
|
||||
|
|
@ -269,37 +259,28 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<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/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}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
|
||||
<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>
|
||||
|
||||
{/* )} */}
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const DocumentTabs = () => {
|
|||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{/* <TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="submitted"
|
||||
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
|
||||
|
|
@ -31,20 +31,20 @@ const DocumentTabs = () => {
|
|||
Waiting Approval
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList> */}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
<div className="bg-white dark:bg-default-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<TableTeks />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={3} />
|
||||
</div>
|
||||
</TabsContent> */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const TeksCreatePage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormTeks />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const TeksDetailPage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormTeksDetail />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,15 @@ 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 dark:bg-default-50">
|
||||
<SiteBreadcrumb />
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* <SiteBreadcrumb /> */}
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white dark:bg-default-50 rounded-t-lg">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -25,32 +21,22 @@ const ReactTableDocumentPage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
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>
|
||||
<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 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>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50 dark:bg-default-50">
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<DocumentTabs />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const AudioVisualTabs = () => {
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
{/* <div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="submitted"
|
||||
|
|
@ -32,19 +32,19 @@ const AudioVisualTabs = () => {
|
|||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
<div className="bg-white dark:bg-default-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<TableVideo />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={2} />
|
||||
</div>
|
||||
</TabsContent> */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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);
|
||||
|
|
@ -195,59 +194,45 @@ const useTableColumns = () => {
|
|||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
statusId,
|
||||
statusName,
|
||||
isPublish,
|
||||
reviewedAtLevel = "",
|
||||
creatorGroupLevelId,
|
||||
needApprovalFromLevel,
|
||||
} = row.original;
|
||||
|
||||
const userLevelId = Number(getCookiesDecrypt("ulie"));
|
||||
const statusId = Number(row.original?.statusId);
|
||||
const reviewedAtLevel = row.original?.reviewedAtLevel || "";
|
||||
const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
|
||||
const needApprovalFromLevel = Number(
|
||||
row.original?.needApprovalFromLevel
|
||||
);
|
||||
|
||||
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||
const isCreator = Number(creatorGroupLevelId) === userLevelId;
|
||||
const isCreator = creatorGroupLevelId === 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 isWaitingForReview =
|
||||
statusId === 2 && !userHasReviewed && !isCreator;
|
||||
const isApprovalNeeded =
|
||||
statusId === 1 && needApprovalFromLevel === Number(userLevelId);
|
||||
|
||||
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 label =
|
||||
isWaitingForReview || isApprovalNeeded
|
||||
? "Menunggu Review"
|
||||
: statusId === 2
|
||||
? "Diterima"
|
||||
: row.original?.statusName;
|
||||
|
||||
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",
|
||||
Diterima: "bg-green-100 text-green-600",
|
||||
default: "bg-red-200 text-red-600",
|
||||
};
|
||||
|
||||
const statusStyles = colors[label] || colors.default;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-full px-5 w-full whitespace-nowrap",
|
||||
statusStyles
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -304,7 +289,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]);
|
||||
|
|
@ -321,15 +306,12 @@ const useTableColumns = () => {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<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={`/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>
|
||||
{/* <Link
|
||||
href={`/contributor/content/video/update/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -340,27 +322,20 @@ const useTableColumns = () => {
|
|||
</Link> */}
|
||||
{/* {(Number(row.original.uploadedById) === Number(userId) ||
|
||||
isMabesApprover) && ( */}
|
||||
|
||||
<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>
|
||||
{/* )} */}
|
||||
|
||||
<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
|
||||
<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>
|
||||
</AccessGuard>
|
||||
|
||||
</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>
|
||||
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteMedia(row.original.id)}
|
||||
|
|
|
|||
|
|
@ -72,14 +72,18 @@ const TableVideo = () => {
|
|||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
const [showData, setShowData] = React.useState("50");
|
||||
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[]>(
|
||||
[]
|
||||
|
|
@ -91,6 +95,7 @@ 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,7 +5,7 @@ const VideoCreatePage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormVideo />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const VideoDetailPage = async () => {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4 bg-slate-100 dark:bg-default-50">
|
||||
<div className="space-y-4 bg-slate-100">
|
||||
<FormVideoDetail />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,15 @@ 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 dark:bg-default-50">
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SiteBreadcrumb />
|
||||
<div className="p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Card className="shadow-sm border-0">
|
||||
<CardHeader className="border-b border-gray-200 bg-white dark:bg-default-50 rounded-t-lg">
|
||||
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||
<CardTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -25,32 +21,22 @@ const ReactTableAudioVisualPage = () => {
|
|||
<UploadIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
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>
|
||||
<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 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>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6 bg-gray-50 dark:bg-default-50">
|
||||
<CardContent className="p-6 bg-gray-50">
|
||||
<AudioVisualTabs />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function AdminPage() {
|
|||
|
||||
return (
|
||||
<motion.div
|
||||
className="h-full overflow-auto"
|
||||
className="h-full overflow-auto bg-gray-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ export default function UserDetailPage() {
|
|||
<p className="text-lg font-mono">{user.statusId}</p>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-600">Keycloak ID</label>
|
||||
<p className="text-xs font-mono break-all p-2 rounded">{user.keycloakId}</p>
|
||||
</div> */}
|
||||
<p className="text-xs font-mono break-all bg-gray-100 p-2 rounded">{user.keycloakId}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-600">Dibuat Oleh</label>
|
||||
|
|
@ -185,10 +185,10 @@ export default function UserDetailPage() {
|
|||
<p className="text-lg">{formatDateToIndonesian(user.updatedAt)}</p>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-600">Foto Profil</label>
|
||||
<p className="text-lg">{user.profilePicturePath ? "Tersedia" : "Tidak ada"}</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Information (only show if data exists) */}
|
||||
|
|
|
|||
|
|
@ -1,783 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import UserForm from "@/components/form/user/user-form";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { cn, getCookiesDecrypt } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
AdministrationLevelList,
|
||||
getListCompetencies,
|
||||
getListEducation,
|
||||
getListSchools,
|
||||
getUserById,
|
||||
updateUserInternal,
|
||||
} from "@/service/management-user/management-user";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import dynamic from "next/dynamic";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { close, error, loading } from "@/config/swal";
|
||||
import { useParams } from "next/navigation";
|
||||
import { getUserLevels } from "@/service/approval-workflows";
|
||||
|
||||
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const sns = [
|
||||
{
|
||||
key: 1,
|
||||
id: "comment",
|
||||
typeId: 1,
|
||||
name: "Komentar Konten",
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
id: "fb",
|
||||
typeId: 2,
|
||||
name: "Facebook",
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
id: "ig",
|
||||
typeId: 3,
|
||||
name: "Instagram",
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
id: "twt",
|
||||
typeId: 4,
|
||||
name: "Twitter",
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
id: "yt",
|
||||
typeId: 5,
|
||||
name: "Youtube",
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
id: "emergency",
|
||||
typeId: 6,
|
||||
name: "Emergency Issue",
|
||||
},
|
||||
{
|
||||
key: 7,
|
||||
id: "email",
|
||||
typeId: 7,
|
||||
name: "Email",
|
||||
},
|
||||
{
|
||||
key: 8,
|
||||
id: "inbox",
|
||||
typeId: 8,
|
||||
name: "Pesan Masuk",
|
||||
},
|
||||
{
|
||||
key: 9,
|
||||
id: "whatsapp",
|
||||
typeId: 9,
|
||||
name: "Whatssapp",
|
||||
},
|
||||
{
|
||||
key: 10,
|
||||
id: "tiktok",
|
||||
typeId: 10,
|
||||
name: "Tiktok",
|
||||
},
|
||||
];
|
||||
|
||||
interface RoleData {
|
||||
id: number;
|
||||
label: string;
|
||||
name: string;
|
||||
value: string;
|
||||
levelNumber: number;
|
||||
}
|
||||
|
||||
const FormSchema = z.object({
|
||||
// level: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
fullname: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
username: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
// role: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
// nrp: z.string({
|
||||
// required_error: "Required",
|
||||
// }),
|
||||
address: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
email: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
phoneNumber: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
password: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
confirmPassword: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
isValidPassword: z.boolean().refine((val) => val === true, {
|
||||
message: "Check Password",
|
||||
}),
|
||||
sns: z.array(z.string()).optional(),
|
||||
education: z.string().optional(),
|
||||
school: z.string().optional(),
|
||||
competency: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function EditUserForm() {
|
||||
export default function EditUserPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id;
|
||||
const MySwal = withReactContent(Swal);
|
||||
const levelName = getCookiesDecrypt("ulnae");
|
||||
const [roleList, setRoleList] = useState<RoleData[]>([]);
|
||||
const [userEducations, setUserEducations] = useState<any>();
|
||||
const [userSchools, setUserSchools] = useState<any>();
|
||||
const [userCompetencies, setUserCompetencies] = useState<any>();
|
||||
const params = useSearchParams();
|
||||
// const userId = params?.id ? Number(params.id) : undefined;
|
||||
const userIdParam = params.get("id");
|
||||
const userId = userIdParam ? Number(userIdParam) : undefined;
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
sns: [],
|
||||
education: "1",
|
||||
school: "4",
|
||||
competency: "2",
|
||||
},
|
||||
});
|
||||
|
||||
const passwordVal = form.watch("password");
|
||||
const confPasswordVal = form.watch("confirmPassword");
|
||||
|
||||
useEffect(() => {
|
||||
getDataAdditional();
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
const initData = async () => {
|
||||
loading();
|
||||
const levels = await initFetch();
|
||||
console.log("LEVEL READY:", levels);
|
||||
const response = await getUserById(String(id));
|
||||
close();
|
||||
|
||||
const list = response?.data?.data;
|
||||
const user = list?.find((item: any) => item.id === Number(id));
|
||||
|
||||
if (!user) {
|
||||
error("User tidak ditemukan");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("RESET WITH USER:", user);
|
||||
|
||||
// 3️⃣ Reset SETELAH level ADA
|
||||
form.reset({
|
||||
fullname: user.fullname ?? "",
|
||||
username: user.username ?? "",
|
||||
email: user.email ?? "",
|
||||
address: user.address ?? "",
|
||||
phoneNumber: user.phoneNumber ?? "",
|
||||
// nrp: user.identityNumber ?? "",
|
||||
// level: String(user.userLevelId),
|
||||
// role: String(user.userRoleId ?? ""),
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
sns: [],
|
||||
education: "",
|
||||
school: "",
|
||||
competency: "",
|
||||
});
|
||||
const handleSuccess = () => {
|
||||
router.push("/admin/management-user");
|
||||
};
|
||||
|
||||
// const initData = async () => {
|
||||
// console.log("PARAM ID:", id);
|
||||
|
||||
// loading();
|
||||
// const response = await getUserById(String(id));
|
||||
// const res = response?.data?.data;
|
||||
// close();
|
||||
// console.log("FULL RESPONSE:", response);
|
||||
// console.log("RESPONSE.DATA:", response?.data);
|
||||
// console.log("RESPONSE.DATA.DATA:", response?.data?.data);
|
||||
// if (Number(res.roleId) > 4) {
|
||||
// form.setValue("fullname", res?.fullname);
|
||||
// form.setValue("username", res?.username);
|
||||
// form.setValue("phoneNumber", res?.phoneNumber);
|
||||
// form.setValue("nrp", res?.memberIdentity);
|
||||
// form.setValue("address", res?.address);
|
||||
// form.setValue("email", res?.email);
|
||||
// form.setValue("role", res?.role?.code);
|
||||
// form.setValue("level", String(res?.userLevelId));
|
||||
// } else {
|
||||
// initFetch();
|
||||
// form.setValue("fullname", res?.fullname);
|
||||
// form.setValue("username", res?.username);
|
||||
// form.setValue("phoneNumber", res?.phoneNumber);
|
||||
// form.setValue("nrp", res?.memberIdentity);
|
||||
// form.setValue("address", res?.address);
|
||||
// form.setValue("email", res?.email);
|
||||
// form.setValue("role", res?.role?.code);
|
||||
// form.setValue("level", String(res?.userLevelId));
|
||||
// }
|
||||
// };
|
||||
|
||||
const initFetch = async () => {
|
||||
const response = await getUserLevels();
|
||||
const res = response?.data?.data ?? [];
|
||||
|
||||
const levelsArr: RoleData[] = res.map((levels: any) => ({
|
||||
id: levels.id,
|
||||
label: levels.aliasName ?? levels.name,
|
||||
name: levels.name,
|
||||
value: String(levels.id),
|
||||
levelNumber: levels.levelNumber,
|
||||
}));
|
||||
|
||||
setRoleList(levelsArr);
|
||||
return levelsArr;
|
||||
const handleCancel = () => {
|
||||
router.push("/admin/management-user");
|
||||
};
|
||||
|
||||
async function getDataAdditional() {
|
||||
const resEducations = await getListEducation();
|
||||
setUserEducations(resEducations?.data?.data);
|
||||
const resSchools = await getListSchools();
|
||||
setUserSchools(resSchools?.data?.data);
|
||||
const resCompetencies = await getListCompetencies();
|
||||
setUserCompetencies(resCompetencies?.data?.data);
|
||||
}
|
||||
|
||||
const rawRoleId = getCookiesDecrypt("urie");
|
||||
const userRoleId = rawRoleId ? Number(rawRoleId) : undefined;
|
||||
|
||||
if (!userRoleId || Number.isNaN(userRoleId)) {
|
||||
error("Role user tidak valid, silakan login ulang");
|
||||
return;
|
||||
}
|
||||
|
||||
async function save(data: z.infer<typeof FormSchema>) {
|
||||
const req = {
|
||||
fullname: data.fullname,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
address: data.address,
|
||||
phoneNumber: data.phoneNumber,
|
||||
// userLevelId: Number(data.level),
|
||||
// userRoleId: userRoleId,
|
||||
};
|
||||
|
||||
// let req: any = {
|
||||
// id: Number(id),
|
||||
// firstName: data.fullname,
|
||||
// username: data.username,
|
||||
// roleId: data.role,
|
||||
// userLevelId: Number(data.level),
|
||||
// memberIdentity: data.nrp,
|
||||
// address: data.address,
|
||||
// email: data.email,
|
||||
// password: data.password,
|
||||
// passwordConf: data.confirmPassword,
|
||||
// isDefault: false,
|
||||
// isAdmin: true,
|
||||
// };
|
||||
|
||||
console.log("USER ROLE ID FROM COOKIE:", rawRoleId, userRoleId);
|
||||
console.log("REQUEST UPDATE:", req);
|
||||
|
||||
loading();
|
||||
const response = await updateUserInternal(Number(id), req);
|
||||
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
close();
|
||||
MySwal.fire({
|
||||
title: "Sukses",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "Oke",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/admin/management-user");
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
MySwal.fire({
|
||||
title: "Simpan Data?",
|
||||
text: "",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "Simpan",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
save(data);
|
||||
}
|
||||
});
|
||||
if (!userId) {
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="text-center">
|
||||
<p className="text-red-500">User ID tidak valid</p>
|
||||
<button
|
||||
onClick={() => router.push("/admin/management-user")}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Kembali ke Management User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 bg-white p-10 w-full"
|
||||
>
|
||||
<p className="text-xl">Data User</p>
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="level"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Pilih Level</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[400px] justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? roleList.find((role) => role.value === field.value)
|
||||
?.label
|
||||
: "Pilih level"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandList>
|
||||
<CommandEmpty>No role found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roleList.map((role) => (
|
||||
<CommandItem
|
||||
value={role.label}
|
||||
key={role.value}
|
||||
onSelect={() => {
|
||||
form.setValue("level", role.value);
|
||||
}}
|
||||
>
|
||||
{role.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
role.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fullname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Lengkap</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Masukkan nama lengkap"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Masukkan username"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-wrap gap-3 w-1/2"
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<FormItem
|
||||
key={role.id}
|
||||
className="flex items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={role.id} />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{role.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
{/* {selectedRole === "OPT-ID" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sns"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div className="mb-4">
|
||||
<FormLabel>Social Media Yang Ditangani</FormLabel>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2 w-1/2">
|
||||
{sns.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name="sns"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.typeId}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
String(item.typeId),
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
String(item.typeId),
|
||||
])
|
||||
: field.onChange(
|
||||
(field.value || []).filter(
|
||||
(value) =>
|
||||
value !== String(item.typeId),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedRole === "KUR-ID" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="education"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pendidikan Terakhir</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{userEducations?.map((edu: any) => (
|
||||
<SelectItem key={edu.id} value={String(edu.id)}>
|
||||
{edu.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="school"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Universitas / Perguruan Tinggi</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{userSchools?.map((edu: any) => (
|
||||
<SelectItem key={edu.id} value={String(edu.id)}>
|
||||
{edu.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="competency"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Kompetensi</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{userCompetencies?.map((edu: any) => (
|
||||
<SelectItem key={edu.id} value={String(edu.id)}>
|
||||
{edu.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nrp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nomor Regitrasi Polri {`(NRP)`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Masukkan NRP"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Alamat</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Masukkan alamat"
|
||||
className="resize-none w-1/2"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Masukkan email"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phoneNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>No. Handphone</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Masukkan nomor handphone"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Konfirmasi Kata Sandi</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Masukkan kata sandi"
|
||||
{...field}
|
||||
className="w-1/2"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<PasswordChecklist
|
||||
rules={["minLength", "specialChar", "number", "capital", "match"]}
|
||||
minLength={8}
|
||||
value={passwordVal || ""}
|
||||
valueAgain={confPasswordVal || ""}
|
||||
onChange={(isValid) => {
|
||||
form.setValue("isValidPassword", isValid);
|
||||
}}
|
||||
className="text-sm"
|
||||
messages={{
|
||||
minLength: "Password harus lebih dari 8 karakter",
|
||||
specialChar: "Password harus memiliki spesial karakter",
|
||||
number: "Password harus memiliki angka",
|
||||
capital: "Password harus memiliki huruf kapital",
|
||||
match: "Password sama",
|
||||
}}
|
||||
/>
|
||||
<Link href="/admin/management-user">
|
||||
<Button type="button" color="primary" variant="outline">
|
||||
Back
|
||||
</Button>
|
||||
</Link>
|
||||
<Button type="submit" color="primary" className="mx-3">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="container mx-auto py-6">
|
||||
<UserForm
|
||||
id={userId}
|
||||
mode="edit"
|
||||
onSuccess={handleSuccess}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function ManagementUser() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<section className="flex flex-col gap-2 bg-white dark:bg-default-50 rounded-lg p-3 mt-5 border">
|
||||
<section className="flex flex-col gap-2 bg-slate-50 dark:bg-black rounded-lg p-3 mt-5 border">
|
||||
<div className="flex justify-between py-3">
|
||||
<p className="text-lg">
|
||||
Data User
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
formatDateToIndonesian,
|
||||
getOnlyDate,
|
||||
htmlToString,
|
||||
} from "@/utils/globals";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import StatusToogle from "./status-toogle";
|
||||
import StaticToogle from "./static-toogle";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
// {
|
||||
// accessorKey: "no",
|
||||
// header: "No",
|
||||
// cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
// },
|
||||
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Judul",
|
||||
cell: ({ row }) => <span>{row.getValue("title")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "categoryName",
|
||||
header: "Kategori",
|
||||
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Tanggal Unggah",
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status Banner",
|
||||
cell: ({ row }) => (
|
||||
<StatusToogle id={row.original.id} initChecked={row.original.isBanner} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${row.original.id}`}
|
||||
>
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default columns;
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import columns from "./banner-column";
|
||||
import { listBanner } from "@/service/service/settings/settings";
|
||||
|
||||
const BannerListTable = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
});
|
||||
const [getData, setGetData] = React.useState<any[]>([]);
|
||||
const dataChange = searchParams?.get("dataChange");
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const table = useReactTable({
|
||||
data: getData,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const pageFromUrl = searchParams?.get("page");
|
||||
if (pageFromUrl) {
|
||||
setPage(Number(pageFromUrl));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (dataChange) {
|
||||
router.push("/admin/settings/banner");
|
||||
}
|
||||
getListBanner();
|
||||
}, [dataChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
getListBanner();
|
||||
// getListStaticBanner();
|
||||
}, [page, showData]);
|
||||
|
||||
async function getListBanner() {
|
||||
try {
|
||||
loading();
|
||||
|
||||
const response = await listBanner();
|
||||
|
||||
const data = Array.isArray(response?.data?.data?.content)
|
||||
? response.data.data.content
|
||||
: [];
|
||||
|
||||
console.log("banner", data);
|
||||
|
||||
setGetData(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching banner list:", error);
|
||||
setGetData([]);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// async function getListBanner() {
|
||||
// loading();
|
||||
// let temp: any;
|
||||
|
||||
// const response = await listBanner();
|
||||
// const data = response?.data?.data?.content;
|
||||
// console.log("banner", data);
|
||||
// setGetData(data);
|
||||
|
||||
// close();
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table className="overflow-hidden mt-10">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BannerListTable;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
formatDateToIndonesian,
|
||||
getOnlyDate,
|
||||
htmlToString,
|
||||
} from "@/utils/globals";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { setBanner } from "@/service/service/settings/settings";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: "No",
|
||||
cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Judul",
|
||||
cell: ({ row }) => <span>{row.getValue("title")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "categoryName",
|
||||
header: "Kategori",
|
||||
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Tanggal Unggah",
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => <span>{row.getValue("statusName")}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleBanner = async (id: number) => {
|
||||
const response = setBanner(id, true);
|
||||
toast({
|
||||
title: "Success",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${row.original.id}`}
|
||||
>
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<a onClick={() => handleBanner(row.original.id)}>
|
||||
Jadikan Banner
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default columns;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { setStaticBanner } from "@/service/service/settings/settings";
|
||||
|
||||
export default function StaticToogle(props: {
|
||||
id: number;
|
||||
initChecked: boolean;
|
||||
}) {
|
||||
const { id, initChecked } = props;
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const changeStaticBanner = async (id: number) => {
|
||||
const response = await setStaticBanner(id);
|
||||
|
||||
if (response?.error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: response?.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success ",
|
||||
});
|
||||
router.push("/admin/settings/banner?dataChange=true");
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id="static-toogle"
|
||||
checked={initChecked}
|
||||
onCheckedChange={(e) => {
|
||||
changeStaticBanner(id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { setBanner } from "@/service/service/settings/settings";
|
||||
|
||||
export default function StatusToogle(props: {
|
||||
id: number;
|
||||
initChecked: boolean;
|
||||
}) {
|
||||
const { id, initChecked } = props;
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const disableBanner = async () => {
|
||||
const response = await setBanner(id, false);
|
||||
|
||||
if (response?.error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: response?.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success ",
|
||||
});
|
||||
router.push("/admin/settings/banner?dataChange=true");
|
||||
};
|
||||
return (
|
||||
<Switch
|
||||
id="status-toogle"
|
||||
checked={initChecked}
|
||||
onCheckedChange={() => disableBanner()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,468 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import CustomPagination from "@/components/table/custom-pagination";
|
||||
import { listDataMedia } from "@/service/service/broadcast/broadcast";
|
||||
import { setBanner } from "@/service/service/settings/settings";
|
||||
|
||||
const ContentListBanner = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [showData, setShowData] = React.useState("9");
|
||||
const [categories, setCategories] = React.useState<any>();
|
||||
const [data, setData] = React.useState<any[]>([]);
|
||||
const [totalData, setTotalData] = React.useState<number>(1);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
});
|
||||
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
|
||||
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
|
||||
const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [bannerCount, setBannerCount] = React.useState<number>(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchBannerCount();
|
||||
}, []);
|
||||
|
||||
async function fetchBannerCount() {
|
||||
try {
|
||||
const res = await listDataMedia(0, "100", "", "", "");
|
||||
const banners = res?.data?.data?.content?.filter(
|
||||
(item: any) => item.isBanner,
|
||||
);
|
||||
setBannerCount(banners?.length || 0);
|
||||
|
||||
setBannerCount(data?.length || 0);
|
||||
} catch (error) {
|
||||
console.error("Error fetching banner count:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleTyping = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(() => {
|
||||
setPage(1);
|
||||
fetchData();
|
||||
}, doneTypingInterval);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [categoryFilter, statusFilter]);
|
||||
|
||||
async function doneTyping() {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const pageFromUrl = searchParams?.get("page");
|
||||
if (pageFromUrl) {
|
||||
setPage(Number(pageFromUrl));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
// setPagination({
|
||||
// pageIndex: 0,
|
||||
// pageSize: Number(showData),
|
||||
// });
|
||||
}, [page, showData]);
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
loading();
|
||||
|
||||
const res = await listDataMedia(
|
||||
page - 1,
|
||||
showData,
|
||||
searchQuery,
|
||||
categoryFilter?.sort().join(","),
|
||||
statusFilter?.sort().join(","),
|
||||
);
|
||||
|
||||
const rawData = res?.data?.data;
|
||||
|
||||
const contentData = Array.isArray(rawData?.content)
|
||||
? rawData.content
|
||||
: [];
|
||||
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * Number(showData) + index + 1;
|
||||
});
|
||||
|
||||
setData(contentData);
|
||||
setTotalData(rawData?.totalElements ?? 0);
|
||||
setTotalPage(rawData?.totalPages ?? 1);
|
||||
|
||||
close();
|
||||
} catch (error) {
|
||||
close();
|
||||
console.error("Error fetching banner data:", error);
|
||||
setData([]);
|
||||
setTotalData(0);
|
||||
setTotalPage(1);
|
||||
}
|
||||
}
|
||||
|
||||
// async function fetchData() {
|
||||
// try {
|
||||
// loading();
|
||||
// const res = await listDataMedia(
|
||||
// page - 1,
|
||||
// showData,
|
||||
// searchQuery,
|
||||
// categoryFilter?.sort().join(","),
|
||||
// statusFilter?.sort().join(",")
|
||||
// );
|
||||
|
||||
// const data = res?.data?.data;
|
||||
// const contentData = data?.content;
|
||||
// contentData.forEach((item: any, index: number) => {
|
||||
// item.no = (page - 1) * Number(showData) + index + 1;
|
||||
// });
|
||||
|
||||
// console.log("contentData : ", data);
|
||||
|
||||
// setData(contentData);
|
||||
// setTotalData(data?.totalElements);
|
||||
// setTotalPage(data?.totalPages);
|
||||
// close();
|
||||
// } catch (error) {
|
||||
// console.error("Error fetching tasks:", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
React.useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
async function getCategories() {
|
||||
const category = await listEnableCategory("");
|
||||
const resCategory = category?.data?.data?.content;
|
||||
setCategories(resCategory);
|
||||
}
|
||||
|
||||
const handleChange = (type: string, id: number, checked: boolean) => {
|
||||
if (type === "category") {
|
||||
if (checked) {
|
||||
const temp: number[] = [...categoryFilter];
|
||||
temp.push(id);
|
||||
setCategoryFilter(temp);
|
||||
} else {
|
||||
const temp = categoryFilter.filter((a) => a !== id);
|
||||
setCategoryFilter(temp);
|
||||
}
|
||||
} else {
|
||||
if (checked) {
|
||||
const temp: number[] = [...statusFilter];
|
||||
temp.push(id);
|
||||
setStatusFilter(temp);
|
||||
} else {
|
||||
const temp = statusFilter.filter((a) => a !== id);
|
||||
setStatusFilter(temp);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (id: number) => {
|
||||
setSelectedItems((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedItems.length === data.length) {
|
||||
setSelectedItems([]);
|
||||
} else {
|
||||
setSelectedItems(data.map((item: any) => Number(item.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleBanner = async (ids: number[]) => {
|
||||
try {
|
||||
// const res = await Promise.all(ids.map((id) => setBanner(id, true)));
|
||||
|
||||
for (const element of ids) {
|
||||
loading();
|
||||
const res = await setBanner(element, true);
|
||||
close();
|
||||
if (res?.error) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
description:
|
||||
"Banner sudah melebihi batas maksimum (4 konten). Silahkan di disable banner Lainnya.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: `item berhasil dijadikan banner.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Gagal",
|
||||
description: "Terjadi kesalahan saat menjadikan banner.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between ">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
handleTyping();
|
||||
}}
|
||||
className="max-w-[300px]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
fetchData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <div className="flex flex-row gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="md" variant="outline">
|
||||
1 - {showData} Data
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56 text-sm">
|
||||
<DropdownMenuRadioGroup
|
||||
value={showData}
|
||||
onValueChange={setShowData}
|
||||
>
|
||||
<DropdownMenuRadioItem value="10">
|
||||
1 - 10 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="20">
|
||||
1 - 20 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="25">
|
||||
1 - 25 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="50">
|
||||
1 - 50 Data
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="md" variant="outline">
|
||||
Filter
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 ">
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<p>Filter</p>
|
||||
<a
|
||||
onClick={() => fetchData()}
|
||||
className="cursor-pointer text-primary"
|
||||
>
|
||||
Simpan
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
|
||||
<p>Kategory</p>
|
||||
{categories?.map((category: any) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={category.id}
|
||||
checked={categoryFilter.includes(category.id)}
|
||||
onCheckedChange={(e) =>
|
||||
handleChange("category", category.id, Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={category.id}
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{category.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-3">Status</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={statusFilter.includes(1)}
|
||||
onCheckedChange={(e) =>
|
||||
handleChange("status", 1, Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Menunggu Review
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={statusFilter.includes(2)}
|
||||
onCheckedChange={(e) =>
|
||||
handleChange("status", 2, Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Diterima
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={statusFilter.includes(3)}
|
||||
onCheckedChange={(e) =>
|
||||
handleChange("status", 3, Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Minta Update{" "}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="accepted"
|
||||
checked={statusFilter.includes(4)}
|
||||
onCheckedChange={(e) =>
|
||||
handleChange("status", 4, Boolean(e))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="accepted"
|
||||
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Ditolak
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div> */}
|
||||
</div>
|
||||
<div>
|
||||
{/* Header select all + action */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedItems.length === data.length}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
<span className="text-black dark:text-white">Pilih Semua</span>
|
||||
</div>
|
||||
{selectedItems.length > 0 && (
|
||||
<Button color="primary" onClick={() => handleBanner(selectedItems)}>
|
||||
Jadikan Banner
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grid Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{data?.map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="relative rounded-lg shadow-md overflow-hidden border border-gray-200"
|
||||
>
|
||||
<div className="absolute top-2 left-2 z-10">
|
||||
<Checkbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onCheckedChange={() => handleSelect(item.id)}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src={item.smallThumbnailLink || "/placeholder.jpg"}
|
||||
alt={item.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
<Link
|
||||
href={`/contributor/content/image/detail/${item?.id}`}
|
||||
className="p-3"
|
||||
>
|
||||
<h4 className="font-semibold text-sm">{item.title}</h4>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
{data && data?.length > 0 ? (
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
) : (
|
||||
<p>No Data</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentListBanner;
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"use client";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import ContentListTable from "./component/table";
|
||||
import { useState } from "react";
|
||||
|
||||
import BannerListTable from "./component/banner-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ContentListBanner from "./component/table";
|
||||
|
||||
export default function AdminBanner() {
|
||||
const [selectedTab, setSelectedTab] = useState("content");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between">
|
||||
{selectedTab === "content" ? "List Media" : " List Banner"}
|
||||
|
||||
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-5">
|
||||
<Button
|
||||
rounded="md"
|
||||
onClick={() => setSelectedTab("content")}
|
||||
className={` hover:text-white
|
||||
${
|
||||
selectedTab === "content"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Konten
|
||||
</Button>
|
||||
<Button
|
||||
rounded="md"
|
||||
onClick={() => setSelectedTab("banner")}
|
||||
className={` hover:text-white
|
||||
${
|
||||
selectedTab === "banner"
|
||||
? "bg-black text-white "
|
||||
: "bg-white text-black "
|
||||
}`}
|
||||
>
|
||||
Banner
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTab === "content" ? (
|
||||
<ContentListBanner />
|
||||
) : (
|
||||
<BannerListTable />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,974 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PlusIcon, MenuIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterMenu,
|
||||
getMasterMenus,
|
||||
getMasterMenuById,
|
||||
createMasterMenu,
|
||||
updateMasterMenu,
|
||||
deleteMasterMenu,
|
||||
} from "@/service/menu-modules";
|
||||
import {
|
||||
MenuAction,
|
||||
getMenuActionsByMenuId,
|
||||
createMenuAction,
|
||||
updateMenuAction,
|
||||
deleteMenuAction,
|
||||
createMenuActionsBatch,
|
||||
} from "@/service/menu-actions";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
|
||||
export default function MenuManagementPage() {
|
||||
const router = useRouter();
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingMenu, setEditingMenu] = useState<MasterMenu | null>(null);
|
||||
const [selectedMenuForActions, setSelectedMenuForActions] = useState<MasterMenu | null>(null);
|
||||
const [menuActions, setMenuActions] = useState<MenuAction[]>([]);
|
||||
const [isActionsDialogOpen, setIsActionsDialogOpen] = useState(false);
|
||||
const [isActionFormOpen, setIsActionFormOpen] = useState(false);
|
||||
const [editingAction, setEditingAction] = useState<MenuAction | null>(null);
|
||||
const [actionFormData, setActionFormData] = useState({
|
||||
actionCode: "",
|
||||
actionName: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
httpMethod: "none",
|
||||
position: 0,
|
||||
});
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
group: "",
|
||||
statusId: 1,
|
||||
parentMenuId: undefined as number | undefined,
|
||||
icon: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const menusRes = await getMasterMenus({ limit: 100 });
|
||||
|
||||
if (menusRes?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: menusRes?.message || "Failed to load menus",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenus([]);
|
||||
} else {
|
||||
// Transform snake_case to camelCase for consistency
|
||||
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
||||
...menu,
|
||||
moduleId: menu.module_id || menu.moduleId,
|
||||
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
||||
statusId: menu.status_id || menu.statusId,
|
||||
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
||||
}));
|
||||
setMenus(menusData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading menus",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenus([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = async (menu?: MasterMenu) => {
|
||||
if (menu) {
|
||||
// Fetch fresh data from API to ensure all fields are loaded correctly
|
||||
try {
|
||||
const res = await getMasterMenuById(menu.id);
|
||||
if (!res?.error && res?.data?.data) {
|
||||
const menuData = res.data.data as any;
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menuData.name || menu.name || "",
|
||||
description: menuData.description || menu.description || "",
|
||||
group: menuData.group || menu.group || "",
|
||||
statusId: menuData.status_id || menuData.statusId || menu.statusId || 1,
|
||||
parentMenuId: menuData.parent_menu_id !== undefined && menuData.parent_menu_id !== null
|
||||
? menuData.parent_menu_id
|
||||
: (menuData.parentMenuId !== undefined && menuData.parentMenuId !== null
|
||||
? menuData.parentMenuId
|
||||
: (menu.parentMenuId || undefined)),
|
||||
icon: menuData.icon || menu.icon || "",
|
||||
});
|
||||
} else {
|
||||
// Fallback to menu object if API call fails
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menu.name || "",
|
||||
description: menu.description || "",
|
||||
group: menu.group || "",
|
||||
statusId: (menu as any).status_id || menu.statusId || 1,
|
||||
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
||||
? (menu as any).parent_menu_id
|
||||
: (menu.parentMenuId || undefined),
|
||||
icon: menu.icon || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menu details:", error);
|
||||
// Fallback to menu object if API call fails
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menu.name || "",
|
||||
description: menu.description || "",
|
||||
group: menu.group || "",
|
||||
statusId: (menu as any).status_id || menu.statusId || 1,
|
||||
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
||||
? (menu as any).parent_menu_id
|
||||
: (menu.parentMenuId || undefined),
|
||||
icon: menu.icon || "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setEditingMenu(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
group: "",
|
||||
statusId: 1,
|
||||
parentMenuId: undefined,
|
||||
icon: "",
|
||||
});
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// Prepare payload without moduleId
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
group: formData.group,
|
||||
statusId: formData.statusId,
|
||||
parentMenuId: formData.parentMenuId,
|
||||
icon: formData.icon,
|
||||
};
|
||||
|
||||
if (editingMenu) {
|
||||
const res = await updateMasterMenu(editingMenu.id, payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Menu updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} else {
|
||||
const res = await createMasterMenu(payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Menu created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving menu:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageActions = async (menu: MasterMenu) => {
|
||||
try {
|
||||
setSelectedMenuForActions(menu);
|
||||
setIsActionsDialogOpen(true);
|
||||
await loadMenuActions(menu.id);
|
||||
} catch (error) {
|
||||
console.error("Error opening actions dialog:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "Failed to open actions management",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenuActions = async (menuId: number) => {
|
||||
try {
|
||||
const res = await getMenuActionsByMenuId(menuId);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to load menu actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenuActions([]);
|
||||
} else {
|
||||
setMenuActions(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menu actions:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading menu actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenuActions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenActionForm = (action?: MenuAction) => {
|
||||
if (action) {
|
||||
setEditingAction(action);
|
||||
setActionFormData({
|
||||
actionCode: action.actionCode,
|
||||
actionName: action.actionName,
|
||||
description: action.description || "",
|
||||
pathUrl: action.pathUrl || "",
|
||||
httpMethod: action.httpMethod || "none",
|
||||
position: action.position || 0,
|
||||
});
|
||||
} else {
|
||||
setEditingAction(null);
|
||||
setActionFormData({
|
||||
actionCode: "",
|
||||
actionName: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
httpMethod: "none",
|
||||
position: menuActions.length + 1,
|
||||
});
|
||||
}
|
||||
setIsActionFormOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveAction = async () => {
|
||||
if (!selectedMenuForActions) return;
|
||||
|
||||
try {
|
||||
// Prepare payload, converting "none" back to undefined for httpMethod
|
||||
const payload = {
|
||||
menuId: selectedMenuForActions.id,
|
||||
actionCode: actionFormData.actionCode,
|
||||
actionName: actionFormData.actionName,
|
||||
description: actionFormData.description || undefined,
|
||||
pathUrl: actionFormData.pathUrl || undefined,
|
||||
httpMethod: actionFormData.httpMethod === "none" ? undefined : actionFormData.httpMethod,
|
||||
position: actionFormData.position,
|
||||
};
|
||||
|
||||
if (editingAction) {
|
||||
const res = await updateMenuAction(editingAction.id, payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Action updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
setIsActionFormOpen(false);
|
||||
}
|
||||
} else {
|
||||
const res = await createMenuAction(payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Action created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
setIsActionFormOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving action:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAction = async (action: MenuAction) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Action?",
|
||||
text: `Are you sure you want to delete "${action.actionName}"?`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMenuAction(action.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Action has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
if (selectedMenuForActions) {
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting action:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAddActions = async () => {
|
||||
if (!selectedMenuForActions) return;
|
||||
|
||||
const standardActions = ["view", "create", "edit", "delete", "approve", "export"];
|
||||
const existingActionCodes = menuActions.map(a => a.actionCode);
|
||||
const newActions = standardActions.filter(code => !existingActionCodes.includes(code));
|
||||
|
||||
if (newActions.length === 0) {
|
||||
Swal.fire({
|
||||
title: "Info",
|
||||
text: "All standard actions already exist",
|
||||
icon: "info",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createMenuActionsBatch({
|
||||
menuId: selectedMenuForActions.id,
|
||||
actionCodes: newActions,
|
||||
});
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: `${newActions.length} actions created successfully`,
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating actions:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (menu: MasterMenu) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Menu?",
|
||||
text: `Are you sure you want to delete "${menu.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMasterMenu(menu.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Menu has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting menu:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Menu Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system menus and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Menu
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingMenu ? `Edit Menu: ${editingMenu.name}` : "Create New Menu"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Menu Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Dashboard, Content Management"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Brief description of the menu"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Group"
|
||||
name="group"
|
||||
type="text"
|
||||
placeholder="e.g., Main, Settings, Content"
|
||||
value={formData.group}
|
||||
onChange={(value) => setFormData({ ...formData, group: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Parent Menu"
|
||||
name="parentMenuId"
|
||||
type="select"
|
||||
placeholder="Select parent menu (optional)"
|
||||
value={formData.parentMenuId || 0}
|
||||
onChange={(value) => setFormData({ ...formData, parentMenuId: Number(value) === 0 ? undefined : Number(value) })}
|
||||
options={[
|
||||
{ value: 0, label: "No Parent (Root Menu)" },
|
||||
...menus
|
||||
.filter((m) => !m.parentMenuId || m.id !== editingMenu?.id)
|
||||
.map((menu) => ({
|
||||
value: menu.id,
|
||||
label: `${menu.name} - ${menu.group}`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<FormField
|
||||
label="Icon"
|
||||
name="icon"
|
||||
type="text"
|
||||
placeholder="e.g., material-symbols:dashboard, heroicons:bars-3"
|
||||
value={formData.icon}
|
||||
onChange={(value) => setFormData({ ...formData, icon: value })}
|
||||
helpText="Icon identifier (e.g., from Iconify)"
|
||||
/>
|
||||
<FormField
|
||||
label="Status ID"
|
||||
name="statusId"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingMenu ? "Update" : "Create"} Menu
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading menus...</p>
|
||||
</div>
|
||||
) : menus.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{menus.map((menu) => {
|
||||
const parentMenu = menus.find((m) => m.id === menu.parentMenuId);
|
||||
return (
|
||||
<Card key={menu.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{menu.name}</span>
|
||||
{menu.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">{menu.description}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Group:</span> {menu.group}
|
||||
</div>
|
||||
{parentMenu && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Parent:</span> {parentMenu.name}
|
||||
</div>
|
||||
)}
|
||||
{menu.icon && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Icon:</span> {menu.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleOpenDialog(menu)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleManageActions(menu)}
|
||||
>
|
||||
<MenuIcon className="h-4 w-4 mr-2" />
|
||||
Actions
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(menu)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<MenuIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Menus Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first menu to define system navigation
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Menu
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Actions Management Dialog */}
|
||||
<Dialog open={isActionsDialogOpen} onOpenChange={setIsActionsDialogOpen}>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent size="lg" className="!max-w-[60vw] !w-[60vw] !min-w-[60vw] max-h-[95vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Manage Actions: {selectedMenuForActions?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
Manage actions available for this menu
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleQuickAddActions}
|
||||
>
|
||||
Quick Add Standard Actions
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleOpenActionForm()}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuActions.length > 0 ? (
|
||||
<div className="border rounded-lg">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Code</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Path URL</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Position</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{menuActions.map((action) => (
|
||||
<tr key={action.id}>
|
||||
<td className="px-4 py-3 text-sm font-medium text-gray-900">{action.actionCode}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.actionName}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.pathUrl || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.httpMethod || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.position || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleOpenActionForm(action)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDeleteAction(action)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">No actions found for this menu</p>
|
||||
<Button onClick={() => handleOpenActionForm()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Add First Action
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Action Form Dialog */}
|
||||
<Dialog open={isActionFormOpen} onOpenChange={setIsActionFormOpen}>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingAction ? `Edit Action: ${editingAction.actionName}` : "Create New Action"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Action Code"
|
||||
name="actionCode"
|
||||
type="text"
|
||||
placeholder="e.g., view, create, edit, delete"
|
||||
value={actionFormData.actionCode}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, actionCode: value })}
|
||||
required
|
||||
helpText="Unique identifier for the action"
|
||||
/>
|
||||
<FormField
|
||||
label="Action Name"
|
||||
name="actionName"
|
||||
type="text"
|
||||
placeholder="e.g., View Content, Create Content"
|
||||
value={actionFormData.actionName}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, actionName: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the action"
|
||||
value={actionFormData.description}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, description: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Path URL"
|
||||
name="pathUrl"
|
||||
type="text"
|
||||
placeholder="e.g., /admin/articles, /api/articles"
|
||||
value={actionFormData.pathUrl}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, pathUrl: value })}
|
||||
helpText="Optional: URL path for routing"
|
||||
/>
|
||||
<FormField
|
||||
label="HTTP Method"
|
||||
name="httpMethod"
|
||||
type="select"
|
||||
placeholder="Select HTTP method"
|
||||
value={actionFormData.httpMethod || "none"}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, httpMethod: value })}
|
||||
options={[
|
||||
{ value: "none", label: "Not specified" },
|
||||
{ value: "GET", label: "GET" },
|
||||
{ value: "POST", label: "POST" },
|
||||
{ value: "PUT", label: "PUT" },
|
||||
{ value: "PATCH", label: "PATCH" },
|
||||
{ value: "DELETE", label: "DELETE" },
|
||||
]}
|
||||
/>
|
||||
<FormField
|
||||
label="Position"
|
||||
name="position"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={actionFormData.position}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, position: Number(value) || 0 })}
|
||||
helpText="Order of action in the menu"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsActionFormOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveAction}>
|
||||
{editingAction ? "Update" : "Create"} Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, SettingsIcon, MenuIcon, ModuleIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterMenu,
|
||||
MasterModule,
|
||||
MenuModule,
|
||||
getMasterMenus,
|
||||
getMasterModules,
|
||||
getMenuModulesByMenuId,
|
||||
createMenuModulesBatch,
|
||||
deleteMenuModule,
|
||||
} from "@/service/menu-modules";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
export default function MenuSettingsPage() {
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
||||
const [selectedMenu, setSelectedMenu] = useState<MasterMenu | null>(null);
|
||||
const [menuModules, setMenuModules] = useState<MenuModule[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [selectedModuleIds, setSelectedModuleIds] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMenu) {
|
||||
loadMenuModules(selectedMenu.id);
|
||||
}
|
||||
}, [selectedMenu]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [menusRes, modulesRes] = await Promise.all([
|
||||
getMasterMenus({ limit: 100 }),
|
||||
getMasterModules({ limit: 100 }),
|
||||
]);
|
||||
|
||||
if (!menusRes?.error) {
|
||||
setMenus(menusRes?.data?.data || []);
|
||||
}
|
||||
if (!modulesRes?.error) {
|
||||
setModules(modulesRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenuModules = async (menuId: number) => {
|
||||
try {
|
||||
const res = await getMenuModulesByMenuId(menuId);
|
||||
if (!res?.error) {
|
||||
setMenuModules(res?.data?.data || []);
|
||||
setSelectedModuleIds((res?.data?.data || []).map((mm: MenuModule) => mm.moduleId));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menu modules:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveModules = async () => {
|
||||
if (!selectedMenu) return;
|
||||
|
||||
try {
|
||||
// Delete existing menu modules
|
||||
for (const menuModule of menuModules) {
|
||||
await deleteMenuModule(menuModule.id);
|
||||
}
|
||||
|
||||
// Create new menu modules in batch
|
||||
if (selectedModuleIds.length > 0) {
|
||||
const res = await createMenuModulesBatch({
|
||||
menuId: selectedMenu.id,
|
||||
moduleIds: selectedModuleIds,
|
||||
});
|
||||
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to save modules",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Modules saved successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuModules(selectedMenu.id);
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving modules:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleModuleSelection = (moduleId: number) => {
|
||||
setSelectedModuleIds((prev) =>
|
||||
prev.includes(moduleId)
|
||||
? prev.filter((id) => id !== moduleId)
|
||||
: [...prev, moduleId]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Menu Settings</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage menu and module associations
|
||||
</p>
|
||||
</div>
|
||||
<MenuIcon className="h-6 w-6 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Menu List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MenuIcon className="h-5 w-5" />
|
||||
Menus
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
</div>
|
||||
) : menus.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{menus.map((menu) => (
|
||||
<button
|
||||
key={menu.id}
|
||||
onClick={() => setSelectedMenu(menu)}
|
||||
className={`w-full text-left p-3 rounded-lg border transition-colors ${
|
||||
selectedMenu?.id === menu.id
|
||||
? "bg-blue-50 border-blue-500 text-blue-900"
|
||||
: "bg-white border-gray-200 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{menu.name}</div>
|
||||
<div className="text-sm text-gray-500">{menu.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No menus found
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Selected Menu Modules */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ModuleIcon className="h-5 w-5" />
|
||||
{selectedMenu ? `${selectedMenu.name} - Modules` : "Select a Menu"}
|
||||
</CardTitle>
|
||||
{selectedMenu && (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Manage Modules
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage Modules for {selectedMenu.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{modules.map((module) => (
|
||||
<label
|
||||
key={module.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModuleIds.includes(module.id)}
|
||||
onChange={() => toggleModuleSelection(module.id)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{module.name}</div>
|
||||
<div className="text-sm text-gray-500">{module.description}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{module.pathUrl} • {module.actionType}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveModules}>
|
||||
Save Modules
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{selectedMenu ? (
|
||||
menuModules.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{menuModules.map((menuModule) => (
|
||||
<div
|
||||
key={menuModule.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{menuModule.module?.name || `Module ${menuModule.moduleId}`}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{menuModule.module?.description || "No description"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{menuModule.position && (
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Position: {menuModule.position}
|
||||
</span>
|
||||
)}
|
||||
{menuModule.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<ModuleIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>No modules assigned to this menu</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Add Modules
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<MenuIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>Select a menu to view its modules</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,501 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterModule,
|
||||
MasterMenu,
|
||||
getMasterModules,
|
||||
getMasterMenus,
|
||||
getMenuModulesByModuleId,
|
||||
createMasterModule,
|
||||
updateMasterModule,
|
||||
deleteMasterModule,
|
||||
createMenuModulesBatch,
|
||||
} from "@/service/menu-modules";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
|
||||
export default function ModuleManagementPage() {
|
||||
const router = useRouter();
|
||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingModule, setEditingModule] = useState<MasterModule | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
menuIds: [] as number[],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getMasterModules({ limit: 100 });
|
||||
if (!res?.error) {
|
||||
setModules(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading modules:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getMasterMenus({ limit: 100 });
|
||||
if (!res?.error) {
|
||||
setMenus(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menus:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadModuleMenus = async (moduleId: number) => {
|
||||
try {
|
||||
const res = await getMenuModulesByModuleId(moduleId);
|
||||
if (!res?.error) {
|
||||
const menuIds = (res?.data?.data || []).map((mm: any) => mm.menu_id || mm.menuId).filter((id: any) => id);
|
||||
setFormData((prev) => ({ ...prev, menuIds }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading module menus:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = async (module?: MasterModule) => {
|
||||
await loadMenus();
|
||||
|
||||
if (module) {
|
||||
setEditingModule(module);
|
||||
setFormData({
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
pathUrl: module.pathUrl,
|
||||
actionType: module.actionType || "",
|
||||
statusId: module.statusId,
|
||||
menuIds: [],
|
||||
});
|
||||
await loadModuleMenus(module.id);
|
||||
} else {
|
||||
setEditingModule(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
menuIds: [],
|
||||
});
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const { menuIds, ...moduleData } = formData;
|
||||
|
||||
if (editingModule) {
|
||||
const res = await updateMasterModule(editingModule.id, moduleData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update menu associations
|
||||
if (menuIds && menuIds.length > 0) {
|
||||
// Create associations for each selected menu
|
||||
for (const menuId of menuIds) {
|
||||
try {
|
||||
await createMenuModulesBatch({
|
||||
menuId,
|
||||
moduleIds: [editingModule.id],
|
||||
});
|
||||
} catch (err) {
|
||||
// Ignore duplicate errors, backend should handle it
|
||||
console.log("Menu association may already exist:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
} else {
|
||||
const res = await createMasterModule(moduleData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the created module ID from response
|
||||
const createdModuleId = res?.data?.data?.id || res?.data?.id;
|
||||
|
||||
// Create menu associations if menuIds provided
|
||||
if (createdModuleId && menuIds && menuIds.length > 0) {
|
||||
for (const menuId of menuIds) {
|
||||
await createMenuModulesBatch({
|
||||
menuId,
|
||||
moduleIds: [createdModuleId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (module: MasterModule) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Module?",
|
||||
text: `Are you sure you want to delete "${module.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMasterModule(module.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Module has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Module Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system modules and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Module
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingModule ? `Edit Module: ${editingModule.name}` : "Create New Module"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Module Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., View Articles, Create Content"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Brief description of the module"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Path URL"
|
||||
name="pathUrl"
|
||||
type="text"
|
||||
placeholder="e.g., /api/articles, /api/content"
|
||||
value={formData.pathUrl}
|
||||
onChange={(value) => setFormData({ ...formData, pathUrl: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Action Type"
|
||||
name="actionType"
|
||||
type="select"
|
||||
placeholder="Select action type"
|
||||
value={formData.actionType}
|
||||
onChange={(value) => setFormData({ ...formData, actionType: value })}
|
||||
options={[
|
||||
{ value: "view", label: "View" },
|
||||
{ value: "create", label: "Create" },
|
||||
{ value: "update", label: "Update" },
|
||||
{ value: "delete", label: "Delete" },
|
||||
{ value: "approve", label: "Approve" },
|
||||
{ value: "reject", label: "Reject" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="menuIds">Menus (Optional)</Label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Select which menus this module belongs to. A module can belong to multiple menus.
|
||||
</p>
|
||||
<div className="border rounded-md p-4 max-h-48 overflow-y-auto">
|
||||
{menus.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{menus.map((menu) => (
|
||||
<div key={menu.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`menu-${menu.id}`}
|
||||
checked={formData.menuIds.includes(menu.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setFormData({
|
||||
...formData,
|
||||
menuIds: [...formData.menuIds, menu.id],
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
menuIds: formData.menuIds.filter((id) => id !== menu.id),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`menu-${menu.id}`}
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{menu.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No menus available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
label="Status ID"
|
||||
name="statusId"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingModule ? "Update" : "Create"} Module
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading modules...</p>
|
||||
</div>
|
||||
) : modules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => (
|
||||
<Card key={module.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{module.name}</span>
|
||||
{module.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">{module.description}</div>
|
||||
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
|
||||
{module.pathUrl}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
{module.actionType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleOpenDialog(module)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(module)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Modules Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first module to define system capabilities
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Module
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterModule,
|
||||
getMasterModules,
|
||||
createMasterModule,
|
||||
updateMasterModule,
|
||||
deleteMasterModule,
|
||||
} from "@/service/menu-modules";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
|
||||
export default function ModulesSettingsPage() {
|
||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingModule, setEditingModule] = useState<MasterModule | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getMasterModules({ limit: 100 });
|
||||
if (!res?.error) {
|
||||
setModules(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading modules:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = (module?: MasterModule) => {
|
||||
if (module) {
|
||||
setEditingModule(module);
|
||||
setFormData({
|
||||
name: module.name ?? "",
|
||||
description: module.description ?? "",
|
||||
pathUrl: module.pathUrl ?? "",
|
||||
actionType: module.actionType ?? "",
|
||||
statusId: module.statusId ?? 1,
|
||||
});
|
||||
} else {
|
||||
setEditingModule(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
});
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (editingModule) {
|
||||
const res = await updateMasterModule(editingModule.id, formData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} else {
|
||||
const res = await createMasterModule(formData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (module: MasterModule) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Module?",
|
||||
text: `Are you sure you want to delete "${module.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMasterModule(module.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Module has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Modules Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system modules and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => handleOpenDialog()}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Module
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingModule
|
||||
? `Edit Module: ${editingModule.name}`
|
||||
: "Create New Module"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Module Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., View Articles, Create Content"
|
||||
value={formData.name}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, name: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Brief description of the module"
|
||||
value={formData.description}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, description: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Path URL"
|
||||
name="pathUrl"
|
||||
type="text"
|
||||
placeholder="e.g., /api/articles, /api/content"
|
||||
value={formData.pathUrl}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, pathUrl: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Action Type"
|
||||
name="actionType"
|
||||
type="select"
|
||||
placeholder="Select action type"
|
||||
value={formData.actionType}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, actionType: value })
|
||||
}
|
||||
options={[
|
||||
{ value: "view", label: "View" },
|
||||
{ value: "create", label: "Create" },
|
||||
{ value: "update", label: "Update" },
|
||||
{ value: "delete", label: "Delete" },
|
||||
{ value: "approve", label: "Approve" },
|
||||
{ value: "reject", label: "Reject" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Status ID"
|
||||
name="statusId"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, statusId: Number(value) || 1 })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingModule ? "Update" : "Create"} Module
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading modules...</p>
|
||||
</div>
|
||||
) : modules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => (
|
||||
<Card
|
||||
key={module.id}
|
||||
className="hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{module.name}</span>
|
||||
{module.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
{module.description}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
|
||||
{module.pathUrl}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
{module.actionType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleOpenDialog(module)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(module)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No Modules Found
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first module to define system capabilities
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Module
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Menubar,
|
||||
MenubarContent,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
} from "@/components/ui/menubar";
|
||||
import EditTagModal from "./edit";
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: "No",
|
||||
cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "tagName",
|
||||
header: "Nama Tag",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("tagName")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "categoryName",
|
||||
header: "Kategori",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">{row.getValue("categoryName")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "contentCount",
|
||||
header: "Jumlah Content",
|
||||
cell: ({ row }) => (
|
||||
<span className="normal-case">
|
||||
{row.getValue("contentCount") ? row.getValue("contentCount") : "-"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const tagDelete = async (id: string) => {
|
||||
// const response = await deleteDataFAQ(id);
|
||||
// console.log(response);
|
||||
// if (response?.error) {
|
||||
// error(response.message);
|
||||
// return false;
|
||||
// }
|
||||
toast({
|
||||
title: "Sukses",
|
||||
description: "Berhasil Delete",
|
||||
});
|
||||
router.push("/admin/settings/tag?dataChange=true");
|
||||
};
|
||||
return (
|
||||
<Menubar className="border-none">
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</MenubarTrigger>
|
||||
<MenubarContent className="flex flex-col gap-2 justify-center items-start p-4">
|
||||
<EditTagModal
|
||||
id={row.original.id}
|
||||
isDetail={true}
|
||||
data={row.original}
|
||||
/>
|
||||
|
||||
<EditTagModal
|
||||
id={row.original.id}
|
||||
isDetail={false}
|
||||
data={row.original}
|
||||
/>
|
||||
|
||||
<a
|
||||
onClick={() => tagDelete(row.original.id)}
|
||||
className="hover:underline cursor-pointer hover:text-destructive"
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</Menubar>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default columns;
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getCategoriesAll } from "@/service/service/settings/settings";
|
||||
import { listArticleCategories } from "@/service/content";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
category: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
});
|
||||
|
||||
export default function CreateTagModal() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const t = useTranslations("Menu");
|
||||
const [categoryList, setCategoryList] = useState<
|
||||
{ id: number; label: string; value: string }[]
|
||||
>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
category: "",
|
||||
},
|
||||
});
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
const request = {
|
||||
tagName: data.name,
|
||||
categoryId: Number(data.category),
|
||||
isActive: true,
|
||||
};
|
||||
// const response = await postDataFeedback(request);
|
||||
// close();
|
||||
// if (response?.error) {
|
||||
// toast({ title: stringify(response.message), variant: "destructive" });
|
||||
// return false;
|
||||
// }
|
||||
toast({
|
||||
title: "Succes",
|
||||
description: "Tag berhasil dibuat",
|
||||
});
|
||||
router.push("/admin/settings/tag?dataChange=true");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await listArticleCategories(1, 100);
|
||||
|
||||
if (response?.error) {
|
||||
console.error("Failed to fetch categories:", response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const categories =
|
||||
response?.data?.data?.map((item: any) => ({
|
||||
value: String(item.id), // wajib string
|
||||
label: item.title, // pakai title dari API
|
||||
})) || [];
|
||||
|
||||
setCategoryList(categories);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch categories:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="primary" size="md">
|
||||
{t("add-tags", { defaultValue: "Add Tags" })}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{" "}
|
||||
{t("add-tags", { defaultValue: "Add Tags" })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white dark:bg-default-50 rounded-sm"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Pilih Category</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[400px] justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? categoryList.find(
|
||||
(categ) => categ.value === field.value,
|
||||
)?.label
|
||||
: "Pilih level"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandList>
|
||||
<CommandEmpty>No role found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{categoryList.map((role) => (
|
||||
<CommandItem
|
||||
value={role.label}
|
||||
key={role.value}
|
||||
onSelect={() => {
|
||||
form.setValue("category", role.value);
|
||||
}}
|
||||
>
|
||||
{role.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
role.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Tag</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Masukkan nama tag" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="md"
|
||||
className="text-xs"
|
||||
>
|
||||
Tambah Tag
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useRouter } from "@/i18n/routing";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { close, error, loading } from "@/config/swal";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { stringify } from "querystring";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getCategoriesAll, postDataFeedback } from "@/service/service/settings/settings";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
category: z.string({
|
||||
required_error: "Required",
|
||||
}),
|
||||
|
||||
// publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
// message: "Required",
|
||||
// }),
|
||||
});
|
||||
|
||||
const publishToList = [
|
||||
{
|
||||
id: "mabes",
|
||||
name: "Nasional",
|
||||
},
|
||||
{
|
||||
id: "polda",
|
||||
name: "Polda",
|
||||
},
|
||||
{
|
||||
id: "satker",
|
||||
name: "Satker",
|
||||
},
|
||||
{
|
||||
id: "internasional",
|
||||
name: "Internasional",
|
||||
},
|
||||
];
|
||||
|
||||
export default function EditTagModal(props: {
|
||||
id: string;
|
||||
isDetail: boolean;
|
||||
data: {
|
||||
id: number;
|
||||
tagName: string;
|
||||
categoryId: number;
|
||||
subCategoryId: number;
|
||||
};
|
||||
}) {
|
||||
const { id, isDetail, data } = props;
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [categoryList, setCategoryList] = useState<
|
||||
{ id: number; label: string; value: string }[]
|
||||
>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [id]);
|
||||
|
||||
const initState = async () => {
|
||||
form.setValue("name", data.tagName);
|
||||
form.setValue("category", String(data.categoryId));
|
||||
};
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||
const request = {
|
||||
id: Number(id),
|
||||
tagName: data.name,
|
||||
categoryId: Number(data.category),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const response = await postDataFeedback(request);
|
||||
close();
|
||||
if (response?.error) {
|
||||
toast({ title: stringify(response.message), variant: "destructive" });
|
||||
return false;
|
||||
}
|
||||
toast({
|
||||
title: "Succes",
|
||||
description: "Tag berhasil diubah",
|
||||
});
|
||||
router.push("/admin/settings/tag?dataChange=true");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCategoryParent();
|
||||
}, []);
|
||||
async function getCategoryParent() {
|
||||
const response = await getCategoriesAll();
|
||||
const res = response?.data?.data.content;
|
||||
console.log("res", res);
|
||||
var levelsArr: { id: number; label: string; value: string }[] = [];
|
||||
res.forEach((levels: { id: number; name: string }) => {
|
||||
levelsArr.push({
|
||||
id: levels.id,
|
||||
label: levels.name,
|
||||
value: String(levels.id),
|
||||
});
|
||||
});
|
||||
setCategoryList(levelsArr);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<a className="hover:underline cursor-pointer">
|
||||
{isDetail ? "Detail" : "Edit"}
|
||||
</a>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isDetail ? "Detail" : "Edit"} Tag</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3 bg-white rounded-sm"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Pilih Category</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild disabled={isDetail}>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[400px] justify-between",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? categoryList.find(
|
||||
(categ) => categ.value === field.value
|
||||
)?.label
|
||||
: "Pilih level"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandList>
|
||||
<CommandEmpty>No role found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{categoryList.map((role) => (
|
||||
<CommandItem
|
||||
value={role.label}
|
||||
key={role.value}
|
||||
onSelect={() => {
|
||||
form.setValue("category", role.value);
|
||||
}}
|
||||
>
|
||||
{role.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
role.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nama Tag</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
readOnly={isDetail}
|
||||
placeholder="Masukkan nama tag"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isDetail && (
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="md"
|
||||
className="text-xs"
|
||||
>
|
||||
Edit Tag
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import columns from "./column";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import CreateFAQModal from "./create";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getTags } from "@/service/content";
|
||||
|
||||
const AdminTagTable = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("Menu");
|
||||
const dataChange = searchParams?.get("dataChange");
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [dataTable, setDataTable] = React.useState<any[]>([]);
|
||||
const [totalData, setTotalData] = React.useState<number>(1);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (dataChange) {
|
||||
router.push("/admin/settings/tag");
|
||||
}
|
||||
fetchData();
|
||||
}, [dataChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const pageFromUrl = searchParams?.get("page");
|
||||
if (pageFromUrl) {
|
||||
setPage(Number(pageFromUrl));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [page]);
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
loading();
|
||||
const response = await getTags();
|
||||
|
||||
const data = Array.isArray(response?.data?.data)
|
||||
? response.data.data
|
||||
: [];
|
||||
|
||||
data.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * 10 + index + 1;
|
||||
});
|
||||
|
||||
setDataTable(data);
|
||||
setTotalData(data.length);
|
||||
setTotalPage(1);
|
||||
close();
|
||||
} catch (error) {
|
||||
close();
|
||||
console.error("Error fetching tags:", error);
|
||||
setDataTable([]);
|
||||
setTotalData(0);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto bg-white dark:bg-default-50 p-4 rounded-sm space-y-3">
|
||||
<div className="flex justify-between mb-10 items-center">
|
||||
<p className="text-xl font-medium text-default-900">
|
||||
{t("tags", { defaultValue: "Tags" })}
|
||||
</p>
|
||||
<CreateFAQModal />
|
||||
</div>
|
||||
|
||||
<Table className="overflow-hidden">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table?.getRowModel()?.rows?.length ? (
|
||||
table?.getRowModel()?.rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/* <TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminTagTable;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import AdminTagTable from "./component/table";
|
||||
|
||||
export default function TagCategory() {
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<AdminTagTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -24,25 +24,6 @@ import {
|
|||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getUserLevelDetail } from "@/service/tenant";
|
||||
import {
|
||||
getUserLevelMenuAccessesByUserLevelId,
|
||||
UserLevelMenuAccess,
|
||||
} from "@/service/user-level-menu-accesses";
|
||||
import {
|
||||
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
|
||||
UserLevelMenuActionAccess,
|
||||
} from "@/service/user-level-menu-action-accesses";
|
||||
import {
|
||||
getMenuActionsByMenuId,
|
||||
MenuAction,
|
||||
} from "@/service/menu-actions";
|
||||
import {
|
||||
getMasterMenus,
|
||||
MasterMenu,
|
||||
} from "@/service/menu-modules";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const useTableColumns = (onEdit?: (data: any) => void) => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -211,68 +192,11 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
const [detailData, setDetailData] = React.useState<any>(null);
|
||||
|
||||
const [menuAccesses, setMenuAccesses] = React.useState<UserLevelMenuAccess[]>([]);
|
||||
const [actionAccesses, setActionAccesses] = React.useState<Record<number, UserLevelMenuActionAccess[]>>({});
|
||||
const [menus, setMenus] = React.useState<MasterMenu[]>([]);
|
||||
const [menuActionsMap, setMenuActionsMap] = React.useState<Record<number, MenuAction[]>>({});
|
||||
const [isLoadingDetail, setIsLoadingDetail] = React.useState(false);
|
||||
|
||||
const handleView = async (id: number) => {
|
||||
setIsLoadingDetail(true);
|
||||
try {
|
||||
// Load basic user level data
|
||||
const res = await getUserLevelDetail(id);
|
||||
if (!res?.error) {
|
||||
setDetailData(res?.data?.data);
|
||||
|
||||
// Load menus
|
||||
const menusRes = await getMasterMenus({ limit: 100 });
|
||||
if (!menusRes?.error) {
|
||||
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
||||
...menu,
|
||||
moduleId: menu.module_id || menu.moduleId,
|
||||
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
||||
statusId: menu.status_id || menu.statusId,
|
||||
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
||||
}));
|
||||
setMenus(menusData);
|
||||
|
||||
// Load actions for each menu
|
||||
const actionsMap: Record<number, MenuAction[]> = {};
|
||||
for (const menu of menusData) {
|
||||
try {
|
||||
const actionsRes = await getMenuActionsByMenuId(menu.id);
|
||||
if (!actionsRes?.error) {
|
||||
actionsMap[menu.id] = actionsRes?.data?.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading actions for menu ${menu.id}:`, error);
|
||||
}
|
||||
}
|
||||
setMenuActionsMap(actionsMap);
|
||||
}
|
||||
|
||||
// Load menu accesses
|
||||
const menuAccessRes = await getUserLevelMenuAccessesByUserLevelId(id);
|
||||
if (!menuAccessRes?.error) {
|
||||
const accesses = menuAccessRes?.data?.data || [];
|
||||
setMenuAccesses(accesses);
|
||||
|
||||
// Load action accesses for each menu
|
||||
const actionAccessesMap: Record<number, UserLevelMenuActionAccess[]> = {};
|
||||
for (const menuAccess of accesses.filter((a: UserLevelMenuAccess) => a.canAccess)) {
|
||||
try {
|
||||
const actionRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(id, menuAccess.menuId);
|
||||
if (!actionRes?.error) {
|
||||
actionAccessesMap[menuAccess.menuId] = actionRes?.data?.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading action accesses for menu ${menuAccess.menuId}:`, error);
|
||||
}
|
||||
}
|
||||
setActionAccesses(actionAccessesMap);
|
||||
}
|
||||
|
||||
setIsDialogOpen(true);
|
||||
} else {
|
||||
error(res?.message || "Gagal memuat detail user level");
|
||||
|
|
@ -280,8 +204,6 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
} catch (err) {
|
||||
console.error("View error:", err);
|
||||
error("Terjadi kesalahan saat memuat data.");
|
||||
} finally {
|
||||
setIsLoadingDetail(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -396,206 +318,41 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
</DropdownMenuContent>
|
||||
{/* ✅ Dialog Detail User Level */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent size="md" className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail User Level</DialogTitle>
|
||||
<DialogDescription>
|
||||
Informasi lengkap dari user level: {detailData?.name || detailData?.aliasName}
|
||||
Informasi lengkap dari user level ID: {detailData?.id}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoadingDetail ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-2 text-gray-600">Memuat data...</span>
|
||||
{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.parentLevelId || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Created At:</span>{" "}
|
||||
{detailData.createdAt
|
||||
? new Date(detailData.createdAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
) : detailData ? (
|
||||
<Tabs defaultValue="basic" className="w-full mt-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger className="border border-black" value="basic">Basic Information</TabsTrigger>
|
||||
<TabsTrigger value="menus">Menu Access</TabsTrigger>
|
||||
<TabsTrigger value="actions">Action Access</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information Tab */}
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Level Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">ID:</span>
|
||||
<p className="text-base font-mono">{detailData.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Name:</span>
|
||||
<p className="text-base">{detailData.name || detailData.aliasName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Alias Name:</span>
|
||||
<p className="text-base font-mono">{detailData.aliasName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Level Number:</span>
|
||||
<p className="text-base">{detailData.levelNumber}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Group:</span>
|
||||
<p className="text-base">{detailData.group || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Parent Level ID:</span>
|
||||
<p className="text-base">{detailData.parentLevelId || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Province ID:</span>
|
||||
<p className="text-base">{detailData.provinceId || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Is Approval Active:</span>
|
||||
<Badge className={detailData.isApprovalActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{detailData.isApprovalActive ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Is Active:</span>
|
||||
<Badge className={detailData.isActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{detailData.isActive ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Created At:</span>
|
||||
<p className="text-sm">
|
||||
{detailData.createdAt
|
||||
? new Date(detailData.createdAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Updated At:</span>
|
||||
<p className="text-sm">
|
||||
{detailData.updatedAt
|
||||
? new Date(detailData.updatedAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Menu Access Tab */}
|
||||
<TabsContent value="menus" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Menu Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess).length > 0 ? (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{menuAccesses
|
||||
.filter((a: UserLevelMenuAccess) => a.canAccess)
|
||||
.map((access) => {
|
||||
const menu = menus.find((m) => m.id === access.menuId);
|
||||
return (
|
||||
<div
|
||||
key={access.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{menu?.name || `Menu ID: ${access.menuId}`}</div>
|
||||
<div className="text-sm text-gray-500">{menu?.description || "-"}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Group: {menu?.group || "-"} • Status: {access.canAccess ? "Accessible" : "No Access"}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={access.canAccess ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{access.canAccess ? "Accessible" : "No Access"}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No menu access configured
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Action Access Tab */}
|
||||
<TabsContent value="actions" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Action Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{Object.keys(actionAccesses).length > 0 ? (
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{Object.entries(actionAccesses).map(([menuId, actions]) => {
|
||||
const menu = menus.find((m) => m.id === Number(menuId));
|
||||
const accessibleActions = actions.filter((a: UserLevelMenuActionAccess) => a.canAccess);
|
||||
|
||||
if (accessibleActions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card key={menuId} className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{menu?.name || `Menu ID: ${menuId}`}</CardTitle>
|
||||
<p className="text-sm text-gray-500">{menu?.description || "-"}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{accessibleActions.map((actionAccess) => {
|
||||
const action = menuActionsMap[Number(menuId)]?.find(
|
||||
(a) => a.actionCode === actionAccess.actionCode
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={actionAccess.id}
|
||||
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">
|
||||
{action?.actionName || actionAccess.actionCode}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Code: {actionAccess.actionCode}
|
||||
{action?.pathUrl && ` • Path: ${action.pathUrl}`}
|
||||
{action?.httpMethod && ` • Method: ${action.httpMethod}`}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800">
|
||||
Allowed
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No action access configured
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<p className="text-gray-500 mt-4">Memuat data...</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-5">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Tutup</Button>
|
||||
<Button onClick={() => setIsDialogOpen(false)}>Tutup</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -55,16 +55,13 @@ 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";
|
||||
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
||||
import { getInfoProfile } from "@/service/auth";
|
||||
|
||||
function TenantSettingsContentTable() {
|
||||
const [activeTab, setActiveTab] = useLocalStorage(
|
||||
"tenant-settings-active-tab",
|
||||
"profile",
|
||||
);
|
||||
const [activeTab, setActiveTab] = useLocalStorage('tenant-settings-active-tab', 'profile');
|
||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||
const [workflow, setWorkflow] =
|
||||
useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
|
|
@ -102,7 +99,7 @@ function TenantSettingsContentTable() {
|
|||
}
|
||||
|
||||
if (!userLevelsRes?.error) {
|
||||
const data = userLevelsRes?.data?.data ?? [];
|
||||
const data = userLevelsRes?.data?.data;
|
||||
data.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * Number(showData) + index + 1;
|
||||
item.parentLevelName =
|
||||
|
|
@ -124,7 +121,7 @@ function TenantSettingsContentTable() {
|
|||
};
|
||||
|
||||
const handleWorkflowSave = async (
|
||||
data: CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
data: CreateApprovalWorkflowWithClientSettingsRequest
|
||||
) => {
|
||||
setIsEditingWorkflow(false);
|
||||
await loadData();
|
||||
|
|
@ -164,7 +161,7 @@ function TenantSettingsContentTable() {
|
|||
|
||||
const columns = React.useMemo(
|
||||
() => useTableColumns((data) => handleEditUserLevel(data)),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const [showData, setShowData] = React.useState("10");
|
||||
const [page, setPage] = React.useState(1);
|
||||
|
|
@ -200,41 +197,24 @@ function TenantSettingsContentTable() {
|
|||
<div className="container mx-auto p-6 space-y-6 border rounded-lg">
|
||||
<div className="flex items-center justify-between ">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Tenant Settings
|
||||
</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Tenant Settings</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage approval workflows and user levels for your tenant
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-6 w-6 text-gray-500" />
|
||||
{/* <Button variant="outline" size="sm" onClick={checkWorkflowStatus}>
|
||||
<Button variant="outline" size="sm" onClick={checkWorkflowStatus}>
|
||||
Check Workflow Status
|
||||
</Button> */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-red-50 text-red-600 border-red-200 hover:bg-red-100"
|
||||
onClick={async () => {
|
||||
const res = await getInfoProfile();
|
||||
const workflowInfo = res?.data?.data?.approvalWorkflowInfo;
|
||||
|
||||
if (workflowInfo) {
|
||||
showWorkflowModal(workflowInfo);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test Modal
|
||||
</Button>
|
||||
{/* <Button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => showWorkflowModal({ hasWorkflowSetup: false })}
|
||||
className="bg-red-50 text-red-600 border-red-200 hover:bg-red-100"
|
||||
>
|
||||
Test Modal
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -274,7 +254,6 @@ function TenantSettingsContentTable() {
|
|||
</h2>
|
||||
{workflow && !isEditingWorkflow && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
|
|
@ -284,7 +263,6 @@ function TenantSettingsContentTable() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* {isEditingWorkflow && workflow && workflow.workflow?.id ? ( */}
|
||||
{isEditingWorkflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
@ -300,8 +278,6 @@ function TenantSettingsContentTable() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<ApprovalWorkflowForm
|
||||
key={workflow?.workflow.id}
|
||||
workflowId={workflow?.workflow.id}
|
||||
initialData={
|
||||
workflow
|
||||
? {
|
||||
|
|
@ -309,12 +285,8 @@ function TenantSettingsContentTable() {
|
|||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval:
|
||||
workflow.clientSettings.requiresApproval,
|
||||
// workflow.workflow.requiresApproval,
|
||||
autoPublish:
|
||||
workflow.clientSettings.autoPublishArticles,
|
||||
// workflow.workflow.autoPublish,
|
||||
requiresApproval: workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps:
|
||||
workflow.steps?.map((step) => ({
|
||||
stepOrder: step.stepOrder,
|
||||
|
|
@ -381,54 +353,44 @@ function TenantSettingsContentTable() {
|
|||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{workflow.workflow.totalSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Total Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workflow.workflow.activeSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Active Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
// workflow.workflow.requiresApproval
|
||||
workflow.clientSettings.requiresApproval
|
||||
workflow.workflow.requiresApproval
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{
|
||||
// workflow.workflow.requiresApproval
|
||||
workflow.clientSettings.requiresApproval ? "Yes" : "No"
|
||||
}
|
||||
{workflow.workflow.requiresApproval ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Requires Approval
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
// workflow.workflow.autoPublish
|
||||
workflow.clientSettings.autoPublishArticles
|
||||
workflow.workflow.autoPublish
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{
|
||||
// workflow.workflow.autoPublish
|
||||
workflow.clientSettings.autoPublishArticles
|
||||
? "Yes"
|
||||
: "No"
|
||||
}
|
||||
{workflow.workflow.autoPublish ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Auto Publish</div>
|
||||
</div>
|
||||
|
|
@ -442,16 +404,14 @@ function TenantSettingsContentTable() {
|
|||
{workflow.steps.map((step: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-300 rounded-lg"
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{step.stepOrder}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-black">
|
||||
{step.stepName}
|
||||
</div>
|
||||
<div className="font-medium">{step.stepName}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{step.conditionType &&
|
||||
`Condition: ${step.conditionType}`}
|
||||
|
|
@ -499,7 +459,7 @@ function TenantSettingsContentTable() {
|
|||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">Client Settings</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Default Workflow
|
||||
</div>
|
||||
|
|
@ -507,7 +467,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.clientSettings.defaultWorkflowName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Auto Publish Articles
|
||||
</div>
|
||||
|
|
@ -523,7 +483,7 @@ function TenantSettingsContentTable() {
|
|||
: "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Requires Approval
|
||||
</div>
|
||||
|
|
@ -539,7 +499,7 @@ function TenantSettingsContentTable() {
|
|||
: "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Settings Active
|
||||
</div>
|
||||
|
|
@ -562,7 +522,7 @@ function TenantSettingsContentTable() {
|
|||
Workflow Statistics
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Total Articles Processed
|
||||
</div>
|
||||
|
|
@ -570,7 +530,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.statistics.totalArticlesProcessed}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Pending Articles
|
||||
</div>
|
||||
|
|
@ -578,7 +538,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.statistics.pendingArticles}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Approved Articles
|
||||
</div>
|
||||
|
|
@ -586,7 +546,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.statistics.approvedArticles}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Rejected Articles
|
||||
</div>
|
||||
|
|
@ -594,7 +554,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.statistics.rejectedArticles}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Average Processing Time
|
||||
</div>
|
||||
|
|
@ -602,7 +562,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.statistics.averageProcessingTime}h
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Most Active Step
|
||||
</div>
|
||||
|
|
@ -619,7 +579,7 @@ function TenantSettingsContentTable() {
|
|||
Workflow Information
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Client ID
|
||||
</div>
|
||||
|
|
@ -627,7 +587,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.workflow.clientId}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Created At
|
||||
</div>
|
||||
|
|
@ -635,7 +595,7 @@ function TenantSettingsContentTable() {
|
|||
{new Date(workflow.workflow.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Updated At
|
||||
</div>
|
||||
|
|
@ -643,7 +603,7 @@ function TenantSettingsContentTable() {
|
|||
{new Date(workflow.workflow.updatedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Workflow ID
|
||||
</div>
|
||||
|
|
@ -651,7 +611,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.workflow.id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Has Branches
|
||||
</div>
|
||||
|
|
@ -665,7 +625,7 @@ function TenantSettingsContentTable() {
|
|||
{workflow.workflow.hasBranches ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-300 rounded-lg">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Max Step Order
|
||||
</div>
|
||||
|
|
@ -708,10 +668,7 @@ function TenantSettingsContentTable() {
|
|||
onOpenChange={setIsUserLevelDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 hover:bg-muted focus-visible:bg-muted"
|
||||
>
|
||||
<Button className="flex items-center gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create User Level
|
||||
</Button>
|
||||
|
|
@ -770,7 +727,7 @@ function TenantSettingsContentTable() {
|
|||
onOpenChange={setIsHierarchyExpanded}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<CardHeader className="cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
|
|
@ -788,7 +745,7 @@ function TenantSettingsContentTable() {
|
|||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{userLevels
|
||||
.filter((ul) => !ul.parentLevelId)
|
||||
.filter((ul) => !ul.parentLevelId) // Root levels
|
||||
.sort((a, b) => a.levelNumber - b.levelNumber)
|
||||
.map((rootLevel) => (
|
||||
<div key={rootLevel.id} className="space-y-2">
|
||||
|
|
@ -798,7 +755,7 @@ function TenantSettingsContentTable() {
|
|||
{rootLevel.levelNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-black">
|
||||
<div className="font-medium">
|
||||
{rootLevel.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
|
|
@ -833,7 +790,7 @@ function TenantSettingsContentTable() {
|
|||
{childLevel.levelNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-black">
|
||||
<div className="font-medium text-sm">
|
||||
{childLevel.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
|
|
@ -865,7 +822,7 @@ function TenantSettingsContentTable() {
|
|||
)}
|
||||
|
||||
<Table className="overflow-hidden mt-3 mx-3">
|
||||
<TableHeader className="sticky top-0 bg-white dark:bg-default-50 shadow-sm z-10">
|
||||
<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) => (
|
||||
|
|
@ -874,7 +831,7 @@ function TenantSettingsContentTable() {
|
|||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
|
|
@ -893,7 +850,7 @@ function TenantSettingsContentTable() {
|
|||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
|
@ -922,21 +879,19 @@ function TenantSettingsContentTable() {
|
|||
</DialogHeader>
|
||||
|
||||
{editingUserLevel ? (
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
<TenantUpdateForm
|
||||
id={editingUserLevel.id}
|
||||
initialData={{
|
||||
id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
provinceId: editingUserLevel.provinceId || 0,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
}}
|
||||
onSave={async (data) => {
|
||||
// The form handles the update internally
|
||||
onSuccess={async () => {
|
||||
setIsEditDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
await loadData();
|
||||
|
|
|
|||
|
|
@ -3,21 +3,8 @@ import React, { useState } from "react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
WorkflowIcon,
|
||||
DotsIcon,
|
||||
DeleteIcon,
|
||||
} from "@/components/icons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, SettingsIcon, UsersIcon, WorkflowIcon, DotsIcon, DeleteIcon } from "@/components/icons";
|
||||
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
||||
|
|
@ -55,14 +42,11 @@ import TenantSettingsPageTable from "./component/tenant-settings-content-table";
|
|||
function TenantSettingsContent() {
|
||||
const [activeTab, setActiveTab] = useState("workflows");
|
||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||
const [workflow, setWorkflow] =
|
||||
useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
const [workflow, setWorkflow] = useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
|
||||
const [editingUserLevel, setEditingUserLevel] = useState<UserLevel | null>(
|
||||
null,
|
||||
);
|
||||
const [editingUserLevel, setEditingUserLevel] = useState<UserLevel | null>(null);
|
||||
const { checkWorkflowStatus } = useWorkflowStatusCheck();
|
||||
const { showWorkflowModal } = useWorkflowModal();
|
||||
|
||||
|
|
@ -95,9 +79,7 @@ function TenantSettingsContent() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleWorkflowSave = async (
|
||||
data: CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
) => {
|
||||
const handleWorkflowSave = async (data: CreateApprovalWorkflowWithClientSettingsRequest) => {
|
||||
setIsEditingWorkflow(false);
|
||||
await loadData(); // Reload data after saving
|
||||
};
|
||||
|
|
@ -128,11 +110,7 @@ function TenantSettingsContent() {
|
|||
};
|
||||
|
||||
const handleDeleteUserLevel = async (userLevel: UserLevel) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to delete "${userLevel.name}"? This action cannot be undone.`,
|
||||
)
|
||||
) {
|
||||
if (window.confirm(`Are you sure you want to delete "${userLevel.name}"? This action cannot be undone.`)) {
|
||||
try {
|
||||
// TODO: Implement delete API call
|
||||
console.log("Delete user level:", userLevel.id);
|
||||
|
|
@ -159,7 +137,11 @@ function TenantSettingsContent() {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-6 w-6 text-gray-500" />
|
||||
<Button variant="outline" size="sm" onClick={checkWorkflowStatus}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={checkWorkflowStatus}
|
||||
>
|
||||
Check Workflow Status
|
||||
</Button>
|
||||
<Button
|
||||
|
|
@ -191,7 +173,6 @@ function TenantSettingsContent() {
|
|||
<h2 className="text-2xl font-semibold">Approval Workflow Setup</h2>
|
||||
{workflow && !isEditingWorkflow && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
|
|
@ -216,47 +197,34 @@ function TenantSettingsContent() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<ApprovalWorkflowForm
|
||||
initialData={
|
||||
workflow
|
||||
? {
|
||||
name: workflow.workflow.name,
|
||||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval: workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps:
|
||||
workflow.steps?.map((step) => ({
|
||||
stepOrder: step.stepOrder,
|
||||
stepName: step.stepName,
|
||||
requiredUserLevelId: step.requiredUserLevelId,
|
||||
canSkip: step.canSkip,
|
||||
autoApproveAfterHours: step.autoApproveAfterHours,
|
||||
isActive: step.isActive,
|
||||
conditionType: step.conditionType,
|
||||
conditionValue: step.conditionValue,
|
||||
})) || [],
|
||||
clientApprovalSettings: {
|
||||
approvalExemptCategories:
|
||||
workflow.clientSettings.exemptCategoriesDetails ||
|
||||
[],
|
||||
approvalExemptRoles:
|
||||
workflow.clientSettings.exemptRolesDetails || [],
|
||||
approvalExemptUsers:
|
||||
workflow.clientSettings.exemptUsersDetails || [],
|
||||
autoPublishArticles:
|
||||
workflow.clientSettings.autoPublishArticles,
|
||||
isActive: workflow.clientSettings.isActive,
|
||||
requireApprovalFor:
|
||||
workflow.clientSettings.requireApprovalFor || [],
|
||||
requiresApproval:
|
||||
workflow.clientSettings.requiresApproval,
|
||||
skipApprovalFor:
|
||||
workflow.clientSettings.skipApprovalFor || [],
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
initialData={workflow ? {
|
||||
name: workflow.workflow.name,
|
||||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval: workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps: workflow.steps?.map(step => ({
|
||||
stepOrder: step.stepOrder,
|
||||
stepName: step.stepName,
|
||||
requiredUserLevelId: step.requiredUserLevelId,
|
||||
canSkip: step.canSkip,
|
||||
autoApproveAfterHours: step.autoApproveAfterHours,
|
||||
isActive: step.isActive,
|
||||
conditionType: step.conditionType,
|
||||
conditionValue: step.conditionValue,
|
||||
})) || [],
|
||||
clientApprovalSettings: {
|
||||
approvalExemptCategories: workflow.clientSettings.exemptCategoriesDetails || [],
|
||||
approvalExemptRoles: workflow.clientSettings.exemptRolesDetails || [],
|
||||
approvalExemptUsers: workflow.clientSettings.exemptUsersDetails || [],
|
||||
autoPublishArticles: workflow.clientSettings.autoPublishArticles,
|
||||
isActive: workflow.clientSettings.isActive,
|
||||
requireApprovalFor: workflow.clientSettings.requireApprovalFor || [],
|
||||
requiresApproval: workflow.clientSettings.requiresApproval,
|
||||
skipApprovalFor: workflow.clientSettings.skipApprovalFor || []
|
||||
}
|
||||
} : undefined}
|
||||
workflowId={workflow?.workflow.id}
|
||||
onSave={handleWorkflowSave}
|
||||
onCancel={() => setIsEditingWorkflow(false)}
|
||||
|
|
@ -293,35 +261,25 @@ function TenantSettingsContent() {
|
|||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{workflow.workflow.totalSteps}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{workflow.workflow.totalSteps}</div>
|
||||
<div className="text-sm text-gray-600">Total Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workflow.workflow.activeSteps}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">{workflow.workflow.activeSteps}</div>
|
||||
<div className="text-sm text-gray-600">Active Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.requiresApproval ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.requiresApproval ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Requires Approval
|
||||
<div className={`text-2xl font-bold ${workflow.workflow.requiresApproval ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.workflow.requiresApproval ? 'Yes' : 'No'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Requires Approval</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.autoPublish ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.autoPublish ? "Yes" : "No"}
|
||||
<div className={`text-2xl font-bold ${workflow.workflow.autoPublish ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.workflow.autoPublish ? 'Yes' : 'No'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Auto Publish</div>
|
||||
</div>
|
||||
|
|
@ -333,10 +291,7 @@ function TenantSettingsContent() {
|
|||
<h4 className="text-lg font-medium mb-3">Workflow Steps</h4>
|
||||
<div className="space-y-2">
|
||||
{workflow.steps.map((step: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{step.stepOrder}
|
||||
|
|
@ -344,12 +299,9 @@ function TenantSettingsContent() {
|
|||
<div>
|
||||
<div className="font-medium">{step.stepName}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{step.conditionType &&
|
||||
`Condition: ${step.conditionType}`}
|
||||
{step.autoApproveAfterHours &&
|
||||
` • Auto-approve after ${step.autoApproveAfterHours}h`}
|
||||
{step.requiredUserLevelName &&
|
||||
` • Required Level: ${step.requiredUserLevelName}`}
|
||||
{step.conditionType && `Condition: ${step.conditionType}`}
|
||||
{step.autoApproveAfterHours && ` • Auto-approve after ${step.autoApproveAfterHours}h`}
|
||||
{step.requiredUserLevelName && ` • Required Level: ${step.requiredUserLevelName}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -391,45 +343,25 @@ function TenantSettingsContent() {
|
|||
<h4 className="text-lg font-medium mb-3">Client Settings</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Default Workflow
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{workflow.clientSettings.defaultWorkflowName}
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Default Workflow</div>
|
||||
<div className="text-sm text-gray-600">{workflow.clientSettings.defaultWorkflowName}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Auto Publish Articles</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.autoPublishArticles ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.autoPublishArticles ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Auto Publish Articles
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-medium ${workflow.clientSettings.autoPublishArticles ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.clientSettings.autoPublishArticles
|
||||
? "Yes"
|
||||
: "No"}
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Requires Approval</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.requiresApproval ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.requiresApproval ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Requires Approval
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-medium ${workflow.clientSettings.requiresApproval ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.clientSettings.requiresApproval
|
||||
? "Yes"
|
||||
: "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Settings Active
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-medium ${workflow.clientSettings.isActive ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.clientSettings.isActive ? "Yes" : "No"}
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Settings Active</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.isActive ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.isActive ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -437,116 +369,68 @@ function TenantSettingsContent() {
|
|||
|
||||
{/* Statistics */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">
|
||||
Workflow Statistics
|
||||
</h4>
|
||||
<h4 className="text-lg font-medium mb-3">Workflow Statistics</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Total Articles Processed
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{workflow.statistics.totalArticlesProcessed}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Total Articles Processed</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{workflow.statistics.totalArticlesProcessed}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Pending Articles
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-yellow-600">
|
||||
{workflow.statistics.pendingArticles}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Pending Articles</div>
|
||||
<div className="text-2xl font-bold text-yellow-600">{workflow.statistics.pendingArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Approved Articles
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workflow.statistics.approvedArticles}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Approved Articles</div>
|
||||
<div className="text-2xl font-bold text-green-600">{workflow.statistics.approvedArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Rejected Articles
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{workflow.statistics.rejectedArticles}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Rejected Articles</div>
|
||||
<div className="text-2xl font-bold text-red-600">{workflow.statistics.rejectedArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Average Processing Time
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{workflow.statistics.averageProcessingTime}h
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Average Processing Time</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{workflow.statistics.averageProcessingTime}h</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Most Active Step
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{workflow.statistics.mostActiveStep || "N/A"}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Most Active Step</div>
|
||||
<div className="text-sm text-gray-600">{workflow.statistics.mostActiveStep || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Metadata */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">
|
||||
Workflow Information
|
||||
</h4>
|
||||
<h4 className="text-lg font-medium mb-3">Workflow Information</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Client ID
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-mono">
|
||||
{workflow.workflow.clientId}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Client ID</div>
|
||||
<div className="text-sm text-gray-600 font-mono">{workflow.workflow.clientId}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Created At
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Created At</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{new Date(workflow.workflow.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Updated At
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Updated At</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{new Date(workflow.workflow.updatedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Workflow ID
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 font-mono">
|
||||
{workflow.workflow.id}
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Workflow ID</div>
|
||||
<div className="text-sm text-gray-600 font-mono">{workflow.workflow.id}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Has Branches</div>
|
||||
<div className={`text-sm font-medium ${workflow.workflow.hasBranches ? 'text-green-600' : 'text-gray-600'}`}>
|
||||
{workflow.workflow.hasBranches ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Has Branches
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-medium ${workflow.workflow.hasBranches ? "text-green-600" : "text-gray-600"}`}
|
||||
>
|
||||
{workflow.workflow.hasBranches ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Max Step Order
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{workflow.workflow.maxStepOrder}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Max Step Order</div>
|
||||
<div className="text-sm text-gray-600">{workflow.workflow.maxStepOrder}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -557,12 +441,9 @@ function TenantSettingsContent() {
|
|||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<WorkflowIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No Workflow Configured
|
||||
</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Workflow Configured</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Set up your approval workflow to manage content approval
|
||||
process
|
||||
Set up your approval workflow to manage content approval process
|
||||
</p>
|
||||
<Button onClick={() => setIsEditingWorkflow(true)}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
|
|
@ -578,10 +459,7 @@ function TenantSettingsContent() {
|
|||
<TabsContent value="user-levels" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">User Levels</h2>
|
||||
<Dialog
|
||||
open={isUserLevelDialogOpen}
|
||||
onOpenChange={setIsUserLevelDialogOpen}
|
||||
>
|
||||
<Dialog open={isUserLevelDialogOpen} onOpenChange={setIsUserLevelDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
|
|
@ -591,28 +469,21 @@ function TenantSettingsContent() {
|
|||
<DialogContent className="md:max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUserLevel
|
||||
? `Edit User Level: ${editingUserLevel.name}`
|
||||
: "Create New User Level"}
|
||||
{editingUserLevel ? `Edit User Level: ${editingUserLevel.name}` : "Create New User Level"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={
|
||||
editingUserLevel
|
||||
? {
|
||||
// id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
initialData={editingUserLevel ? {
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
} : undefined}
|
||||
onSave={handleUserLevelSave}
|
||||
onCancel={() => {
|
||||
setIsUserLevelDialogOpen(false);
|
||||
|
|
@ -627,29 +498,27 @@ function TenantSettingsContent() {
|
|||
{userLevels.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{userLevels.length}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{userLevels.length}</div>
|
||||
<div className="text-sm text-gray-600">Total User Levels</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{userLevels.filter((ul) => ul.isActive).length}
|
||||
{userLevels.filter(ul => ul.isActive).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Active Levels</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{userLevels.filter((ul) => ul.isApprovalActive).length}
|
||||
{userLevels.filter(ul => ul.isApprovalActive).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Approval Active</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{userLevels.filter((ul) => ul.parentLevelId).length}
|
||||
{userLevels.filter(ul => ul.parentLevelId).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Child Levels</div>
|
||||
</div>
|
||||
|
|
@ -668,9 +537,9 @@ function TenantSettingsContent() {
|
|||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{userLevels
|
||||
.filter((ul) => !ul.parentLevelId)
|
||||
.filter(ul => !ul.parentLevelId) // Root levels
|
||||
.sort((a, b) => a.levelNumber - b.levelNumber)
|
||||
.map((rootLevel) => (
|
||||
.map(rootLevel => (
|
||||
<div key={rootLevel.id} className="space-y-2">
|
||||
{/* Root Level */}
|
||||
<div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-500">
|
||||
|
|
@ -680,8 +549,7 @@ function TenantSettingsContent() {
|
|||
<div className="flex-1">
|
||||
<div className="font-medium">{rootLevel.name}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{rootLevel.aliasName} •{" "}
|
||||
{rootLevel.group || "No group"}
|
||||
{rootLevel.aliasName} • {rootLevel.group || 'No group'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -720,23 +588,17 @@ function TenantSettingsContent() {
|
|||
|
||||
{/* Child Levels */}
|
||||
{userLevels
|
||||
.filter((ul) => ul.parentLevelId === rootLevel.id)
|
||||
.filter(ul => ul.parentLevelId === rootLevel.id)
|
||||
.sort((a, b) => a.levelNumber - b.levelNumber)
|
||||
.map((childLevel) => (
|
||||
<div
|
||||
key={childLevel.id}
|
||||
className="ml-8 flex items-center gap-3 p-3 bg-gray-50 rounded-lg border-l-4 border-gray-300"
|
||||
>
|
||||
.map(childLevel => (
|
||||
<div key={childLevel.id} className="ml-8 flex items-center gap-3 p-3 bg-gray-50 rounded-lg border-l-4 border-gray-300">
|
||||
<div className="w-6 h-6 bg-gray-100 text-gray-600 rounded-full flex items-center justify-center text-xs font-medium">
|
||||
{childLevel.levelNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">
|
||||
{childLevel.name}
|
||||
</div>
|
||||
<div className="font-medium text-sm">{childLevel.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{childLevel.aliasName} •{" "}
|
||||
{childLevel.group || "No group"}
|
||||
{childLevel.aliasName} • {childLevel.group || 'No group'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -755,9 +617,7 @@ function TenantSettingsContent() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleEditUserLevel(childLevel)
|
||||
}
|
||||
onClick={() => handleEditUserLevel(childLevel)}
|
||||
className="h-7 w-7 p-0 border-gray-300 hover:border-blue-500 hover:bg-blue-50"
|
||||
title="Edit User Level"
|
||||
>
|
||||
|
|
@ -766,9 +626,7 @@ function TenantSettingsContent() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleDeleteUserLevel(childLevel)
|
||||
}
|
||||
onClick={() => handleDeleteUserLevel(childLevel)}
|
||||
className="h-7 w-7 p-0 border-gray-300 hover:border-red-500 hover:bg-red-50"
|
||||
title="Delete User Level"
|
||||
>
|
||||
|
|
@ -871,9 +729,7 @@ function TenantSettingsContent() {
|
|||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<UsersIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No User Levels Found
|
||||
</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No User Levels Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first user level to define approval hierarchy
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,978 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
EditIcon,
|
||||
SaveIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
WorkflowIcon,
|
||||
} from "@/components/icons";
|
||||
import {
|
||||
Tenant,
|
||||
TenantUpdateRequest,
|
||||
getTenantById,
|
||||
updateTenant,
|
||||
} from "@/service/tenant";
|
||||
import { getTenantList } from "@/service/tenant";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||
import {
|
||||
CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
UserLevelsCreateRequest,
|
||||
UserLevel,
|
||||
getUserLevels,
|
||||
getApprovalWorkflowComprehensiveDetails,
|
||||
ComprehensiveWorkflowResponse,
|
||||
createUserLevel,
|
||||
} from "@/service/approval-workflows";
|
||||
import TenantCompanyUpdateForm from "@/components/form/tenant/tenant-detail-update-form";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import useTableColumns from "@/app/[locale]/(admin)/admin/settings/tenant/component/columns";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { PlusIcon, DeleteIcon } from "@/components/icons";
|
||||
import { errorAutoClose, successAutoClose } from "@/lib/swal";
|
||||
import { close, loading } from "@/config/swal";
|
||||
|
||||
export default function EditTenantPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const tenantId = params?.id as string;
|
||||
const [totalData, setTotalData] = useState<number>(0);
|
||||
const [totalPage, setTotalPage] = useState<number>(1);
|
||||
|
||||
const [tenant, setTenant] = useState<Tenant | null>(null);
|
||||
const [parentTenants, setParentTenants] = useState<Tenant[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("profile");
|
||||
|
||||
// Workflow state
|
||||
const [workflow, setWorkflow] =
|
||||
useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
|
||||
|
||||
// User Levels state
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||
const [editingUserLevel, setEditingUserLevel] = useState<UserLevel | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Tenant form data
|
||||
const [formData, setFormData] = useState<TenantUpdateRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenantId) {
|
||||
loadData();
|
||||
}
|
||||
}, [tenantId, router]);
|
||||
|
||||
// Load workflow and user levels when switching to those tabs
|
||||
useEffect(() => {
|
||||
if (tenant && (activeTab === "workflows" || activeTab === "user-levels")) {
|
||||
loadWorkflowAndUserLevels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [tenantRes, parentRes] = await Promise.all([
|
||||
getTenantById(tenantId),
|
||||
getTenantList({ clientType: "parent_client", limit: 100 }),
|
||||
]);
|
||||
|
||||
if (tenantRes?.error || !tenantRes?.data?.data) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: tenantRes?.message || "Failed to load tenant data",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/tenants");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantData = tenantRes.data.data;
|
||||
setTenant(tenantData);
|
||||
|
||||
// Set form data with all available fields
|
||||
setFormData({
|
||||
name: tenantData.name || "",
|
||||
description: tenantData.description || "",
|
||||
clientType: tenantData.clientType || "standalone",
|
||||
parentClientId: tenantData.parentClientId || undefined,
|
||||
maxUsers: tenantData.maxUsers || undefined,
|
||||
maxStorage: tenantData.maxStorage || undefined,
|
||||
address: tenantData.address || "",
|
||||
phoneNumber: tenantData.phoneNumber || "",
|
||||
website: tenantData.website || "",
|
||||
isActive:
|
||||
tenantData.isActive !== undefined ? tenantData.isActive : true,
|
||||
logoUrl: tenantData.logoUrl || undefined,
|
||||
logoImagePath: tenantData.logoImagePath || undefined,
|
||||
});
|
||||
|
||||
if (!parentRes?.error) {
|
||||
setParentTenants(parentRes?.data?.data || []);
|
||||
}
|
||||
|
||||
// Load workflow and user levels if on those tabs
|
||||
// Note: This will be loaded when tab changes via useEffect
|
||||
} catch (error) {
|
||||
console.error("Error loading tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading tenant data",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/tenants");
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWorkflowAndUserLevels = async () => {
|
||||
try {
|
||||
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
|
||||
getApprovalWorkflowComprehensiveDetails(),
|
||||
getUserLevels(),
|
||||
]);
|
||||
|
||||
if (!comprehensiveWorkflowRes?.error) {
|
||||
setWorkflow(comprehensiveWorkflowRes?.data?.data || null);
|
||||
} else {
|
||||
setWorkflow(null);
|
||||
}
|
||||
if (!userLevelsRes?.error) {
|
||||
const data = userLevelsRes?.data?.data || [];
|
||||
|
||||
setUserLevels(data);
|
||||
setTotalData(data.length);
|
||||
|
||||
const pageSize = pagination.pageSize;
|
||||
setTotalPage(Math.max(1, Math.ceil(data.length / pageSize)));
|
||||
}
|
||||
|
||||
if (!userLevelsRes?.error) {
|
||||
setUserLevels(userLevelsRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading workflow and user levels:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveTenantInfo = async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const updateData: TenantUpdateRequest = {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
clientType: formData.clientType,
|
||||
parentClientId: formData.parentClientId || undefined,
|
||||
maxUsers: formData.maxUsers,
|
||||
maxStorage: formData.maxStorage,
|
||||
address: formData.address || undefined,
|
||||
phoneNumber: formData.phoneNumber || undefined,
|
||||
website: formData.website || undefined,
|
||||
isActive: formData.isActive,
|
||||
};
|
||||
|
||||
const res = await updateTenant(tenantId, updateData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Tenant updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkflowSave = async (
|
||||
data: CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
) => {
|
||||
setIsEditingWorkflow(false);
|
||||
await loadWorkflowAndUserLevels();
|
||||
};
|
||||
|
||||
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
||||
try {
|
||||
loading();
|
||||
const response = await createUserLevel(data);
|
||||
close();
|
||||
|
||||
if (response?.error) {
|
||||
errorAutoClose(response.message || "Failed to create user level.");
|
||||
return;
|
||||
}
|
||||
|
||||
successAutoClose("User level created successfully.");
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
setTimeout(async () => {
|
||||
await loadWorkflowAndUserLevels();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
close();
|
||||
errorAutoClose("An error occurred while creating user level.");
|
||||
console.error("Error creating user level:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUserLevel = (userLevel: UserLevel) => {
|
||||
setEditingUserLevel(userLevel);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteUserLevel = async (userLevel: UserLevel) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete User Level?",
|
||||
text: `Are you sure you want to delete "${userLevel.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// TODO: Implement delete API call
|
||||
console.log("Delete user level:", userLevel.id);
|
||||
await loadWorkflowAndUserLevels();
|
||||
} catch (error) {
|
||||
console.error("Error deleting user level:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => useTableColumns((data) => handleEditUserLevel(data)),
|
||||
[],
|
||||
);
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: userLevels,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading tenant data...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push("/admin/tenants")}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Edit Tenant: {tenant.name}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage tenant information, workflows, and user levels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="profile" className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
Tenant Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="workflows" className="flex items-center gap-2">
|
||||
<WorkflowIcon className="h-4 w-4" />
|
||||
Approval Workflows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="user-levels"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<UsersIcon className="h-4 w-4" />
|
||||
User Levels
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tenant Information Tab */}
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tenant Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<FormField
|
||||
label="Tenant Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Company ABC, Organization XYZ"
|
||||
value={formData.name}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, name: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the tenant"
|
||||
value={formData.description || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
description: value || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
label="Client Type"
|
||||
name="clientType"
|
||||
type="select"
|
||||
placeholder="Select client type"
|
||||
value={formData.clientType}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, clientType: value as any })
|
||||
}
|
||||
options={[
|
||||
{ value: "standalone", label: "Standalone" },
|
||||
{ value: "parent_client", label: "Parent Client" },
|
||||
{ value: "sub_client", label: "Sub Client" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
{formData.clientType === "sub_client" && (
|
||||
<FormField
|
||||
label="Parent Tenant"
|
||||
name="parentClientId"
|
||||
type="select"
|
||||
placeholder="Select parent tenant"
|
||||
value={formData.parentClientId || "none"}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
parentClientId: value === "none" ? undefined : value,
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ value: "none", label: "No Parent Tenant" },
|
||||
...parentTenants
|
||||
.filter((t) => t.id !== tenantId)
|
||||
.map((t) => ({
|
||||
value: t.id,
|
||||
label: t.name,
|
||||
})),
|
||||
]}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Max Users"
|
||||
name="maxUsers"
|
||||
type="number"
|
||||
placeholder="e.g., 100"
|
||||
value={formData.maxUsers?.toString() || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxUsers: value ? Number(value) : undefined,
|
||||
})
|
||||
}
|
||||
helpText="Maximum number of users allowed"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Storage (MB)"
|
||||
name="maxStorage"
|
||||
type="number"
|
||||
placeholder="e.g., 10000"
|
||||
value={formData.maxStorage?.toString() || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxStorage: value ? Number(value) : undefined,
|
||||
})
|
||||
}
|
||||
helpText="Maximum storage in MB"
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Address"
|
||||
name="address"
|
||||
type="textarea"
|
||||
placeholder="Tenant address"
|
||||
value={formData.address || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, address: value || undefined })
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
name="phoneNumber"
|
||||
type="tel"
|
||||
placeholder="e.g., +62 123 456 7890"
|
||||
value={formData.phoneNumber || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
phoneNumber: value || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
label="Website"
|
||||
name="website"
|
||||
type="url"
|
||||
placeholder="e.g., https://example.com"
|
||||
value={formData.website || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, website: value || undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Status"
|
||||
name="isActive"
|
||||
type="select"
|
||||
placeholder="Select status"
|
||||
value={formData.isActive ? "active" : "inactive"}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, isActive: value === "active" })
|
||||
}
|
||||
options={[
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<Button
|
||||
onClick={handleSaveTenantInfo}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{isSaving ? "Saving..." : "Save Tenant Information"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Approval Workflows Tab */}
|
||||
<TabsContent value="workflows" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
Approval Workflow Setup
|
||||
</h2>
|
||||
{workflow && !isEditingWorkflow && (
|
||||
<Button variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
Edit Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditingWorkflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Setup Approval Workflow</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ApprovalWorkflowForm
|
||||
initialData={
|
||||
workflow
|
||||
? {
|
||||
name: workflow.workflow.name,
|
||||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval:
|
||||
workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps:
|
||||
workflow.steps?.map((step) => ({
|
||||
stepOrder: step.stepOrder,
|
||||
stepName: step.stepName,
|
||||
requiredUserLevelId: step.requiredUserLevelId,
|
||||
canSkip: step.canSkip,
|
||||
autoApproveAfterHours:
|
||||
step.autoApproveAfterHours,
|
||||
isActive: step.isActive,
|
||||
conditionType: step.conditionType,
|
||||
conditionValue: step.conditionValue,
|
||||
})) || [],
|
||||
clientApprovalSettings: {
|
||||
approvalExemptCategories:
|
||||
workflow.clientSettings
|
||||
.exemptCategoriesDetails || [],
|
||||
approvalExemptRoles:
|
||||
workflow.clientSettings.exemptRolesDetails ||
|
||||
[],
|
||||
approvalExemptUsers:
|
||||
workflow.clientSettings.exemptUsersDetails ||
|
||||
[],
|
||||
autoPublishArticles:
|
||||
workflow.clientSettings.autoPublishArticles,
|
||||
isActive: workflow.clientSettings.isActive,
|
||||
requireApprovalFor:
|
||||
workflow.clientSettings.requireApprovalFor ||
|
||||
[],
|
||||
requiresApproval:
|
||||
workflow.clientSettings.requiresApproval,
|
||||
skipApprovalFor:
|
||||
workflow.clientSettings.skipApprovalFor || [],
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
workflowId={workflow?.workflow.id}
|
||||
onSave={handleWorkflowSave}
|
||||
onCancel={() => setIsEditingWorkflow(false)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : workflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>{workflow.workflow.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.workflow.isDefault && (
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
{workflow.workflow.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{workflow.workflow.description}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{workflow.workflow.totalSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Total Steps</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workflow.workflow.activeSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Active Steps</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.requiresApproval ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.requiresApproval ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Requires Approval
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.autoPublish ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.autoPublish ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Auto Publish</div>
|
||||
</div>
|
||||
</div>
|
||||
{workflow.steps && workflow.steps.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-lg font-medium mb-3">
|
||||
Workflow Steps
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{workflow.steps.map((step: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{step.stepOrder}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{step.stepName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Required Level: {step.requiredUserLevelName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{step.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<WorkflowIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No Workflow Found
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
No approval workflow has been set up for this tenant yet.
|
||||
</p>
|
||||
<Button onClick={() => setIsEditingWorkflow(true)}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* User Levels Tab */}
|
||||
<TabsContent value="user-levels" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">User Levels Management</h2>
|
||||
<Dialog
|
||||
open={isUserLevelDialogOpen}
|
||||
onOpenChange={setIsUserLevelDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setEditingUserLevel(null);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUserLevel
|
||||
? `Edit User Level: ${editingUserLevel.name}`
|
||||
: "Create New User Level"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={
|
||||
editingUserLevel
|
||||
? {
|
||||
// id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSave={handleUserLevelSave}
|
||||
onCancel={() => {
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{userLevels.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{table
|
||||
.getHeaderGroups()
|
||||
.map((headerGroup) =>
|
||||
headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)),
|
||||
)}
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleEditUserLevel(row.original)
|
||||
}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() =>
|
||||
handleDeleteUserLevel(row.original)
|
||||
}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="p-4">
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<UsersIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No User Levels Found
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first user level to define user hierarchy
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingUserLevel(null);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,463 +0,0 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
Tenant,
|
||||
TenantCreateRequest,
|
||||
getTenantList,
|
||||
createTenant,
|
||||
deleteTenant,
|
||||
} from "@/service/tenant";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export default function TenantsManagementPage() {
|
||||
const router = useRouter();
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [parentTenants, setParentTenants] = useState<Tenant[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [formData, setFormData] = useState<TenantCreateRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [tenantsRes, parentRes] = await Promise.all([
|
||||
getTenantList({ limit: 100 }),
|
||||
getTenantList({ clientType: "parent_client", limit: 100 }),
|
||||
]);
|
||||
|
||||
if (!tenantsRes?.error) {
|
||||
setTenants(tenantsRes?.data?.data || []);
|
||||
}
|
||||
if (!parentRes?.error) {
|
||||
setParentTenants(parentRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading tenants:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const createData: TenantCreateRequest = {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
clientType: formData.clientType,
|
||||
parentClientId: formData.parentClientId || undefined,
|
||||
maxUsers: formData.maxUsers,
|
||||
maxStorage: formData.maxStorage,
|
||||
address: formData.address || undefined,
|
||||
phoneNumber: formData.phoneNumber || undefined,
|
||||
website: formData.website || undefined,
|
||||
};
|
||||
|
||||
const res = await createTenant(createData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Tenant created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (tenant: Tenant) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Tenant?",
|
||||
text: `Are you sure you want to delete "${tenant.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteTenant(tenant.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Tenant has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Tenant Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system tenants and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Tenant
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{/* @ts-ignore - DialogContent accepts children */}
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Tenant</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Tenant Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Company ABC, Organization XYZ"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the tenant"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Client Type"
|
||||
name="clientType"
|
||||
type="select"
|
||||
placeholder="Select client type"
|
||||
value={formData.clientType}
|
||||
onChange={(value) => setFormData({ ...formData, clientType: value as any })}
|
||||
options={[
|
||||
{ value: "standalone", label: "Standalone" },
|
||||
{ value: "parent_client", label: "Parent Client" },
|
||||
{ value: "sub_client", label: "Sub Client" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
{formData.clientType === "sub_client" && (
|
||||
<FormField
|
||||
label="Parent Tenant"
|
||||
name="parentClientId"
|
||||
type="select"
|
||||
placeholder="Select parent tenant"
|
||||
value={formData.parentClientId || "none"}
|
||||
onChange={(value) => setFormData({ ...formData, parentClientId: value === "none" ? undefined : value })}
|
||||
options={[
|
||||
{ value: "none", label: "Select a parent tenant" },
|
||||
...parentTenants.map((tenant) => ({
|
||||
value: tenant.id,
|
||||
label: tenant.name,
|
||||
})),
|
||||
]}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Max Users"
|
||||
name="maxUsers"
|
||||
type="number"
|
||||
placeholder="e.g., 100"
|
||||
value={formData.maxUsers?.toString() || ""}
|
||||
onChange={(value) => setFormData({ ...formData, maxUsers: value ? Number(value) : undefined })}
|
||||
helpText="Maximum number of users allowed"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Storage (MB)"
|
||||
name="maxStorage"
|
||||
type="number"
|
||||
placeholder="e.g., 10000"
|
||||
value={formData.maxStorage?.toString() || ""}
|
||||
onChange={(value) => setFormData({ ...formData, maxStorage: value ? Number(value) : undefined })}
|
||||
helpText="Maximum storage in MB"
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Address"
|
||||
name="address"
|
||||
type="textarea"
|
||||
placeholder="Tenant address"
|
||||
value={formData.address}
|
||||
onChange={(value) => setFormData({ ...formData, address: value })}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
name="phoneNumber"
|
||||
type="tel"
|
||||
placeholder="e.g., +62 123 456 7890"
|
||||
value={formData.phoneNumber}
|
||||
onChange={(value) => setFormData({ ...formData, phoneNumber: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Website"
|
||||
name="website"
|
||||
type="url"
|
||||
placeholder="e.g., https://example.com"
|
||||
value={formData.website}
|
||||
onChange={(value) => setFormData({ ...formData, website: value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Create Tenant
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading tenants...</p>
|
||||
</div>
|
||||
) : tenants.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Parent</TableHead>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Phone</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tenants.map((tenant) => {
|
||||
const parentTenant = tenants.find((t) => t.id === tenant.parentClientId);
|
||||
return (
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell className="font-medium">{tenant.name}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
tenant.clientType === "parent_client"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: tenant.clientType === "sub_client"
|
||||
? "bg-purple-100 text-purple-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}>
|
||||
{tenant.clientType.replace("_", " ")}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{parentTenant?.name || "-"}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">{tenant.address || "-"}</TableCell>
|
||||
<TableCell>{tenant.phoneNumber || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{tenant.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/tenants/${tenant.id}/edit`)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(tenant)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 text-gray-400 mx-auto mb-4 flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className="w-12 h-12"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Tenants Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first tenant to get started
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Tenant
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -6,52 +6,21 @@ 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}>
|
||||
{/* 🔐 Permission hanya untuk admin */}
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<WorkflowModalProvider>
|
||||
<ThemeCustomize />
|
||||
<DashCodeHeader />
|
||||
<DashCodeSidebar />
|
||||
<LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
<DashCodeFooter />
|
||||
</WorkflowModalProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<WorkflowModalProvider>
|
||||
<ThemeCustomize />
|
||||
<DashCodeHeader />
|
||||
<DashCodeSidebar />
|
||||
<LayoutContentProvider>{children}</LayoutContentProvider>
|
||||
<DashCodeFooter />
|
||||
</WorkflowModalProvider>
|
||||
</LayoutProvider>
|
||||
</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;
|
||||
|
|
|
|||
|
|
@ -70,9 +70,7 @@ const AuthPage = () => {
|
|||
const handleOTPSuccess = async () => {
|
||||
if (loginCredentials) {
|
||||
try {
|
||||
await login(loginCredentials, { skipRedirect: false });
|
||||
|
||||
// await login(loginCredentials);
|
||||
await login(loginCredentials);
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Login failed after OTP verification");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,142 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ================================
|
||||
GLOBAL CSS VARIABLE (LIGHT MODE)
|
||||
================================ */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 215.3 19.3% 34.5%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--success: 154 52% 55%;
|
||||
--warning: 16 93% 70%;
|
||||
--info: 185 96% 51%;
|
||||
|
||||
--sidebar: 0 0% 100%;
|
||||
--sidebar-foreground: 215 20% 65%;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
DARK MODE VARIABLES
|
||||
================================ */
|
||||
.dark {
|
||||
--background: 222.2 47.4% 11.2%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 215 27.9% 16.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 215.3 25% 26.7%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--sidebar: 215 27.9% 16.9%;
|
||||
--sidebar-foreground: 214.3 31.8% 91.4%;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
BASE LAYER
|
||||
================================ */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
GLOBAL UTILITIES
|
||||
================================ */
|
||||
@layer utilities {
|
||||
/* SweetAlert z-index fix */
|
||||
.swal-z-index-9999 {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Scrollbar hide */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Input group helpers */
|
||||
.input-group :not(:first-child) input {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.input-group.merged :not(:first-child) input {
|
||||
border-left-width: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.input-group :not(:last-child) input {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.input-group.merged :not(:last-child) input {
|
||||
border-right-width: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* @import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* SweetAlert2 z-index fix */
|
||||
.swal-z-index-9999 {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
|
@ -432,4 +297,4 @@
|
|||
.no-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import Navbar from "@/components/landing-page/navbar";
|
|||
export default function Home() {
|
||||
return (
|
||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="relative z-10 bg-white dark:bg-default-50 w-full mx-auto">
|
||||
<div className="relative z-10 bg-white w-full mx-auto">
|
||||
<Navbar />
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
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}</>;
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||
const { login } = useAuth();
|
||||
const t = useTranslations("MediaUpdate");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState("5");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
|
@ -62,45 +62,21 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||
|
||||
const handleLogin = async (data: LoginFormData) => {
|
||||
try {
|
||||
await login(data, { skipRedirect: true });
|
||||
// await login(data);
|
||||
await login(data);
|
||||
onSuccess?.(data);
|
||||
} catch (error: any) {
|
||||
const message = getLoginErrorMessage(error);
|
||||
onError?.(message);
|
||||
}
|
||||
};
|
||||
|
||||
// const onSubmit = async (data: LoginFormData) => {
|
||||
// try {
|
||||
// // onSuccess?.(data);
|
||||
// await handleLogin(data);
|
||||
// } catch (error: any) {
|
||||
// onError?.(error.message || "Login failed");
|
||||
// }
|
||||
// };
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
try {
|
||||
onSuccess?.(data); // hanya kirim data ke AuthPage
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || "Login failed");
|
||||
}
|
||||
};
|
||||
|
||||
const getLoginErrorMessage = (error: any): string => {
|
||||
const data = error?.response?.data;
|
||||
|
||||
// Backend Netidhub
|
||||
if (Array.isArray(data?.messages) && data.messages.length > 0) {
|
||||
return data.messages[0];
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
try {
|
||||
// Pass the form data to the parent component
|
||||
// The auth page will handle email validation and flow transitions
|
||||
onSuccess?.(data);
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || "Login failed");
|
||||
}
|
||||
|
||||
// Fallback lain
|
||||
if (typeof data?.message === "string") return data.message;
|
||||
if (typeof error?.message === "string") return error.message;
|
||||
|
||||
return "Username atau kata sandi salah";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -201,20 +177,6 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||
{/* Remember Me and Forgot Password */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
id="rememberMe"
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
disabled={isSubmitting}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||
/>
|
||||
<label htmlFor="rememberMe" className="text-sm cursor-pointer">
|
||||
{t("rememberMe")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="rememberMe"
|
||||
checked={rememberMe}
|
||||
|
|
@ -224,7 +186,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
|||
<Label htmlFor="rememberMe" className="text-sm">
|
||||
{t("rememberMe")}
|
||||
</Label>
|
||||
</div> */}
|
||||
</div>
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium hover:underline"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
|||
const [otpValue, setOtpValue] = useState("");
|
||||
const t = useTranslations("MediaUpdate");
|
||||
|
||||
|
||||
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const { key } = event;
|
||||
const target = event.currentTarget;
|
||||
|
|
@ -58,12 +57,9 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
|||
|
||||
try {
|
||||
const isValid = await verifyOTP(loginCredentials.username, otpValue);
|
||||
|
||||
|
||||
if (isValid) {
|
||||
onSuccess?.();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
onError?.("Invalid OTP code");
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
|
@ -158,6 +154,7 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
|||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
|
||||
</div>
|
||||
) : (
|
||||
t("enterOTP4")
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
"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;
|
||||
};
|
||||
|
|
@ -2,213 +2,40 @@
|
|||
|
||||
import React from "react";
|
||||
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||
import Editor from "ckeditor5-custom-build";
|
||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||
|
||||
function CustomEditor(props) {
|
||||
const maxHeight = props.maxHeight || 600;
|
||||
|
||||
return (
|
||||
<div className="ckeditor-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
onChange={(event, editor) => {
|
||||
const data = editor.getData();
|
||||
console.log({ event, editor, data });
|
||||
props.onChange(data);
|
||||
}}
|
||||
config={{
|
||||
toolbar: [
|
||||
"heading",
|
||||
"fontsize",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"numberedList",
|
||||
"bulletedList",
|
||||
"undo",
|
||||
"redo",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"codeBlock",
|
||||
"sourceEditing",
|
||||
],
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #111 !important;
|
||||
background: #fff !important;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
color: inherit !important;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
border-left: 4px solid #d1d5db;
|
||||
background-color: #f9fafb;
|
||||
color: inherit !important;
|
||||
}
|
||||
`,
|
||||
height: props.height || 400,
|
||||
removePlugins: ["Title"],
|
||||
mobile: {
|
||||
theme: "silver",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.ckeditor-wrapper {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.ckeditor-wrapper :global(.ck.ck-editor__main) {
|
||||
min-height: ${props.height || 400}px;
|
||||
max-height: ${maxHeight}px;
|
||||
}
|
||||
|
||||
.ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
||||
min-height: ${(props.height || 400) - 50}px;
|
||||
max-height: ${maxHeight - 50}px;
|
||||
overflow-y: auto !important;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
background: #fff !important;
|
||||
color: #111 !important;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable) {
|
||||
background: #111 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h1),
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h2),
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h3),
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h4),
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h5),
|
||||
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h6) {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable blockquote) {
|
||||
background-color: #1f2937 !important;
|
||||
border-left-color: #374151 !important;
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling for webkit browsers */
|
||||
.ckeditor-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar */
|
||||
:global(.dark)
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
/* Ensure content doesn't overflow */
|
||||
.ckeditor-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
onChange={(event, editor) => {
|
||||
const data = editor.getData();
|
||||
console.log({ event, editor, data });
|
||||
props.onChange(data);
|
||||
}}
|
||||
config={{
|
||||
toolbar: [
|
||||
"heading",
|
||||
"fontsize",
|
||||
"bold",
|
||||
"italic",
|
||||
"link",
|
||||
"numberedList",
|
||||
"bulletedList",
|
||||
"undo",
|
||||
"redo",
|
||||
"alignment",
|
||||
"outdent",
|
||||
"indent",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"codeBlock",
|
||||
"sourceEditing",
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomEditor;
|
||||
|
||||
// // components/custom-editor.js
|
||||
|
||||
// import React from "react";
|
||||
// import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||
// import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||
|
||||
// function CustomEditor(props) {
|
||||
// return (
|
||||
// <CKEditor
|
||||
// editor={Editor}
|
||||
// data={props.initialData}
|
||||
// onChange={(event, editor) => {
|
||||
// const data = editor.getData();
|
||||
// console.log({ event, editor, data });
|
||||
// props.onChange(data);
|
||||
// }}
|
||||
// config={{
|
||||
// toolbar: [
|
||||
// "heading",
|
||||
// "fontsize",
|
||||
// "bold",
|
||||
// "italic",
|
||||
// "link",
|
||||
// "numberedList",
|
||||
// "bulletedList",
|
||||
// "undo",
|
||||
// "redo",
|
||||
// "alignment",
|
||||
// "outdent",
|
||||
// "indent",
|
||||
// "blockQuote",
|
||||
// "insertTable",
|
||||
// "codeBlock",
|
||||
// "sourceEditing",
|
||||
// ],
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default CustomEditor;
|
||||
|
|
|
|||
|
|
@ -1,171 +1,19 @@
|
|||
import React from "react";
|
||||
import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||
import Editor from "ckeditor5-custom-build";
|
||||
import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||
|
||||
function ViewEditor(props) {
|
||||
const maxHeight = props.maxHeight || 600; // Default max height 600px
|
||||
|
||||
return (
|
||||
<div className="ckeditor-view-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
disabled={true}
|
||||
config={{
|
||||
isReadOnly: true,
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #111;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 1em;
|
||||
border-left: 4px solid #d1d5db;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
`,
|
||||
height: props.height || 400,
|
||||
removePlugins: ["Title"],
|
||||
}}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.ckeditor-view-wrapper {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||
min-height: ${props.height || 400}px;
|
||||
max-height: ${maxHeight}px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||
min-height: ${(props.height || 400) - 50}px;
|
||||
max-height: ${maxHeight - 50}px;
|
||||
overflow-y: auto !important;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
background-color: #fdfdfd;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* 🌙 Dark mode support */
|
||||
:global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||
background-color: #111 !important;
|
||||
color: #f9fafb !important;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
:global(.dark) .ckeditor-view-wrapper h1,
|
||||
:global(.dark) .ckeditor-view-wrapper h2,
|
||||
:global(.dark) .ckeditor-view-wrapper h3,
|
||||
:global(.dark) .ckeditor-view-wrapper h4,
|
||||
:global(.dark) .ckeditor-view-wrapper h5,
|
||||
:global(.dark) .ckeditor-view-wrapper h6 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
:global(.dark) .ckeditor-view-wrapper blockquote {
|
||||
background-color: #1f2937 !important;
|
||||
border-left: 4px solid #374151 !important;
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* 🌙 Dark mode scrollbar */
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
/* Read-only specific styling */
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Hide toolbar */
|
||||
.ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||
display: none !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
disabled={true}
|
||||
config={{
|
||||
// toolbar: [],
|
||||
isReadOnly: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewEditor;
|
||||
|
||||
// import React from "react";
|
||||
// import { CKEditor } from "@ckeditor/ckeditor5-react";
|
||||
// import Editor from "@/vendor/ckeditor5/build/ckeditor";
|
||||
|
||||
// function ViewEditor(props) {
|
||||
// return (
|
||||
// <CKEditor
|
||||
// editor={Editor}
|
||||
// data={props.initialData}
|
||||
// disabled={true}
|
||||
// config={{
|
||||
// // toolbar: [],
|
||||
// isReadOnly: true,
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default ViewEditor;
|
||||
|
|
|
|||
|
|
@ -33,18 +33,6 @@ interface ApprovalWorkflowFormProps {
|
|||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const normalizeClientSettings = (settings: ClientApprovalSettingsRequest) => ({
|
||||
approvalExemptCategories: settings.approvalExemptCategories ?? [],
|
||||
approvalExemptRoles: settings.approvalExemptRoles ?? [],
|
||||
approvalExemptUsers: settings.approvalExemptUsers ?? [],
|
||||
requireApprovalFor: settings.requireApprovalFor ?? [],
|
||||
skipApprovalFor: settings.skipApprovalFor ?? [],
|
||||
|
||||
autoPublishArticles: settings.autoPublishArticles ?? false,
|
||||
requiresApproval: settings.requiresApproval ?? false,
|
||||
isActive: settings.isActive ?? false,
|
||||
});
|
||||
|
||||
export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
||||
initialData,
|
||||
workflowId,
|
||||
|
|
@ -53,34 +41,31 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
isLoading = false,
|
||||
}) => {
|
||||
// Form state
|
||||
const [formData, setFormData] =
|
||||
useState<CreateApprovalWorkflowWithClientSettingsRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
isActive: true,
|
||||
isDefault: true,
|
||||
const [formData, setFormData] = useState<CreateApprovalWorkflowWithClientSettingsRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
isActive: true,
|
||||
isDefault: true,
|
||||
requiresApproval: true,
|
||||
autoPublish: false,
|
||||
steps: [],
|
||||
clientApprovalSettings: {
|
||||
requiresApproval: true,
|
||||
autoPublish: false,
|
||||
steps: [],
|
||||
clientApprovalSettings: {
|
||||
requiresApproval: true,
|
||||
autoPublishArticles: false,
|
||||
approvalExemptUsers: [],
|
||||
approvalExemptRoles: [],
|
||||
approvalExemptCategories: [],
|
||||
requireApprovalFor: [],
|
||||
skipApprovalFor: [],
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
autoPublishArticles: false,
|
||||
approvalExemptUsers: [],
|
||||
approvalExemptRoles: [],
|
||||
approvalExemptCategories: [],
|
||||
requireApprovalFor: [],
|
||||
skipApprovalFor: [],
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
// API data
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
|
||||
const [articleCategories, setArticleCategories] = useState<ArticleCategory[]>(
|
||||
[],
|
||||
);
|
||||
const [articleCategories, setArticleCategories] = useState<ArticleCategory[]>([]);
|
||||
|
||||
// UI state
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
|
@ -97,22 +82,19 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
if (stepIndex !== currentStepIndex && step.conditionValue) {
|
||||
try {
|
||||
const conditionData = JSON.parse(step.conditionValue);
|
||||
if (
|
||||
conditionData.applies_to_levels &&
|
||||
Array.isArray(conditionData.applies_to_levels)
|
||||
) {
|
||||
if (conditionData.applies_to_levels && Array.isArray(conditionData.applies_to_levels)) {
|
||||
conditionData.applies_to_levels.forEach((levelId: number) => {
|
||||
usedLevelIds.add(levelId);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing conditionValue:", error);
|
||||
console.error('Error parsing conditionValue:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out used levels and return available ones
|
||||
return userLevels.filter((level) => !usedLevelIds.has(level.id));
|
||||
return userLevels.filter(level => !usedLevelIds.has(level.id));
|
||||
};
|
||||
|
||||
// Load initial data
|
||||
|
|
@ -126,20 +108,17 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [userLevelsRes, usersRes, userRolesRes, categoriesRes] =
|
||||
await Promise.all([
|
||||
getUserLevels(),
|
||||
getUsers(),
|
||||
getUserRoles(),
|
||||
getArticleCategories(),
|
||||
]);
|
||||
const [userLevelsRes, usersRes, userRolesRes, categoriesRes] = await Promise.all([
|
||||
getUserLevels(),
|
||||
getUsers(),
|
||||
getUserRoles(),
|
||||
getArticleCategories(),
|
||||
]);
|
||||
|
||||
if (!userLevelsRes?.error)
|
||||
setUserLevels(userLevelsRes?.data?.data || []);
|
||||
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
|
||||
if (!usersRes?.error) setUsers(usersRes?.data || []);
|
||||
if (!userRolesRes?.error) setUserRoles(userRolesRes?.data || []);
|
||||
if (!categoriesRes?.error)
|
||||
setArticleCategories(categoriesRes?.data || []);
|
||||
if (!categoriesRes?.error) setArticleCategories(categoriesRes?.data || []);
|
||||
} catch (error) {
|
||||
console.error("Error loading form data:", error);
|
||||
} finally {
|
||||
|
|
@ -176,12 +155,10 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
newErrors[`steps.${index}.stepName`] = "Step name is required";
|
||||
}
|
||||
if (!step.requiredUserLevelId) {
|
||||
newErrors[`steps.${index}.requiredUserLevelId`] =
|
||||
"Required user level is required";
|
||||
newErrors[`steps.${index}.requiredUserLevelId`] = "Required user level is required";
|
||||
}
|
||||
if (step.stepOrder <= 0) {
|
||||
newErrors[`steps.${index}.stepOrder`] =
|
||||
"Step order must be greater than 0";
|
||||
newErrors[`steps.${index}.stepOrder`] = "Step order must be greater than 0";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -190,13 +167,10 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
};
|
||||
|
||||
// Form handlers
|
||||
const handleBasicInfoChange = (
|
||||
field: keyof CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
value: any,
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
const handleBasicInfoChange = (field: keyof CreateApprovalWorkflowWithClientSettingsRequest, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
setErrors(prev => ({ ...prev, [field]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -215,23 +189,14 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
return step;
|
||||
});
|
||||
|
||||
setFormData((prev) => ({ ...prev, steps: updatedSteps }));
|
||||
setFormData(prev => ({ ...prev, steps: updatedSteps }));
|
||||
};
|
||||
|
||||
const renderStepForm = (
|
||||
step: ApprovalWorkflowStepRequest,
|
||||
index: number,
|
||||
onUpdate: (step: ApprovalWorkflowStepRequest) => void,
|
||||
onDelete: () => void,
|
||||
) => {
|
||||
const stepErrors = Object.keys(errors).filter((key) =>
|
||||
key.startsWith(`steps.${index}`),
|
||||
);
|
||||
const renderStepForm = (step: ApprovalWorkflowStepRequest, index: number, onUpdate: (step: ApprovalWorkflowStepRequest) => void, onDelete: () => void) => {
|
||||
const stepErrors = Object.keys(errors).filter(key => key.startsWith(`steps.${index}`));
|
||||
|
||||
// Check if this step has parallel steps (same stepOrder)
|
||||
const parallelSteps = formData.steps.filter(
|
||||
(s, i) => s.stepOrder === step.stepOrder && i !== index,
|
||||
);
|
||||
const parallelSteps = formData.steps.filter((s, i) => s.stepOrder === step.stepOrder && i !== index);
|
||||
const isParallelStep = parallelSteps.length > 0;
|
||||
|
||||
return (
|
||||
|
|
@ -257,12 +222,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
type="number"
|
||||
placeholder="1"
|
||||
value={step.stepOrder}
|
||||
onChange={(value) =>
|
||||
onUpdate({
|
||||
...step,
|
||||
stepOrder: value ? Number(value) : index + 1,
|
||||
})
|
||||
}
|
||||
onChange={(value) => onUpdate({ ...step, stepOrder: value ? Number(value) : index + 1 })}
|
||||
error={errors[`steps.${index}.stepOrder`]}
|
||||
min={1}
|
||||
helpText="Same order = parallel steps"
|
||||
|
|
@ -283,31 +243,17 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
label="Required User Level"
|
||||
name={`requiredUserLevelId-${index}`}
|
||||
type="select"
|
||||
placeholder={
|
||||
userLevels.length > 0
|
||||
? "Select user level"
|
||||
: "No user levels available"
|
||||
}
|
||||
placeholder={userLevels.length > 0 ? "Select user level" : "No user levels available"}
|
||||
value={step.requiredUserLevelId}
|
||||
onChange={(value) =>
|
||||
onUpdate({ ...step, requiredUserLevelId: Number(value) })
|
||||
}
|
||||
onChange={(value) => onUpdate({ ...step, requiredUserLevelId: Number(value) })}
|
||||
error={errors[`steps.${index}.requiredUserLevelId`]}
|
||||
options={
|
||||
userLevels.length > 0
|
||||
? userLevels.map((level) => ({
|
||||
value: level.id,
|
||||
label: `${level.name} (Level ${level.levelNumber})`,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
options={userLevels.length > 0 ? userLevels.map(level => ({
|
||||
value: level.id,
|
||||
label: `${level.name} (Level ${level.levelNumber})`,
|
||||
})) : []}
|
||||
required
|
||||
disabled={userLevels.length === 0}
|
||||
helpText={
|
||||
userLevels.length === 0
|
||||
? "No user levels found. Please create user levels first."
|
||||
: undefined
|
||||
}
|
||||
helpText={userLevels.length === 0 ? "No user levels found. Please create user levels first." : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -318,12 +264,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
type="number"
|
||||
placeholder="Leave empty for manual approval"
|
||||
value={step.autoApproveAfterHours}
|
||||
onChange={(value) =>
|
||||
onUpdate({
|
||||
...step,
|
||||
autoApproveAfterHours: value ? Number(value) : undefined,
|
||||
})
|
||||
}
|
||||
onChange={(value) => onUpdate({ ...step, autoApproveAfterHours: value ? Number(value) : undefined })}
|
||||
helpText="Automatically approve after specified hours"
|
||||
min={1}
|
||||
/>
|
||||
|
|
@ -353,35 +294,25 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
label="Applies to User Levels"
|
||||
placeholder={(() => {
|
||||
const availableLevels = getAvailableUserLevels(index);
|
||||
return availableLevels.length > 0
|
||||
? "Select user levels..."
|
||||
: "No available user levels";
|
||||
return availableLevels.length > 0 ? "Select user levels..." : "No available user levels";
|
||||
})()}
|
||||
options={(() => {
|
||||
const availableLevels = getAvailableUserLevels(index);
|
||||
return availableLevels.map((level) => ({
|
||||
return availableLevels.map(level => ({
|
||||
value: level.id,
|
||||
label: `${level.name} (Level ${level.levelNumber})`,
|
||||
}));
|
||||
})()}
|
||||
value={(() => {
|
||||
try {
|
||||
return step.conditionValue
|
||||
? JSON.parse(step.conditionValue).applies_to_levels || []
|
||||
: [];
|
||||
return step.conditionValue ? JSON.parse(step.conditionValue).applies_to_levels || [] : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})()}
|
||||
onChange={(value) => {
|
||||
const conditionValue = JSON.stringify({
|
||||
applies_to_levels: value,
|
||||
});
|
||||
onUpdate({
|
||||
...step,
|
||||
conditionType: "user_level_hierarchy",
|
||||
conditionValue,
|
||||
});
|
||||
const conditionValue = JSON.stringify({ applies_to_levels: value });
|
||||
onUpdate({ ...step, conditionType: "user_level_hierarchy", conditionValue });
|
||||
}}
|
||||
searchable={true}
|
||||
disabled={getAvailableUserLevels(index).length === 0}
|
||||
|
|
@ -403,10 +334,6 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
function normalizeBoolean(value?: boolean): boolean {
|
||||
return value ?? false;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -417,8 +344,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -435,23 +362,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
isActive: true,
|
||||
requireApprovalFor: [],
|
||||
requiresApproval: true,
|
||||
skipApprovalFor: [],
|
||||
};
|
||||
|
||||
const clientSettingsPayload = {
|
||||
approvalExemptCategories: [],
|
||||
approvalExemptRoles: [],
|
||||
approvalExemptUsers: [],
|
||||
requireApprovalFor: [],
|
||||
skipApprovalFor: [],
|
||||
|
||||
autoPublishArticles:
|
||||
formData.clientApprovalSettings.autoPublishArticles ?? false,
|
||||
|
||||
requiresApproval:
|
||||
formData.clientApprovalSettings.requiresApproval ?? false,
|
||||
|
||||
isActive: formData.clientApprovalSettings.isActive ?? false,
|
||||
skipApprovalFor: []
|
||||
};
|
||||
|
||||
if (workflowId) {
|
||||
|
|
@ -460,60 +371,30 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
workflowId,
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
|
||||
isActive: normalizeBoolean(formData.isActive),
|
||||
isDefault: normalizeBoolean(formData.isDefault),
|
||||
|
||||
steps: formData.steps.map((step) => ({
|
||||
isActive: formData.isActive || false,
|
||||
isDefault: formData.isDefault || false,
|
||||
steps: formData.steps.map(step => ({
|
||||
...step,
|
||||
branchName: step.stepName,
|
||||
branchName: step.stepName, // branchName should be same as stepName
|
||||
})),
|
||||
|
||||
clientSettings: {
|
||||
approvalExemptCategories: [],
|
||||
approvalExemptRoles: [],
|
||||
approvalExemptUsers: [],
|
||||
requireApprovalFor: [],
|
||||
skipApprovalFor: [],
|
||||
|
||||
// 🔥 AMBIL LANGSUNG DARI CHECKBOX UI
|
||||
requiresApproval: normalizeBoolean(formData.requiresApproval),
|
||||
autoPublishArticles: normalizeBoolean(formData.autoPublish),
|
||||
isActive: normalizeBoolean(formData.isActive),
|
||||
},
|
||||
clientSettings: hardcodedClientSettings
|
||||
};
|
||||
|
||||
// const updateData: UpdateApprovalWorkflowWithClientSettingsRequest = {
|
||||
// workflowId,
|
||||
// name: formData.name,
|
||||
// description: formData.description,
|
||||
// isActive: formData.isActive,
|
||||
// isDefault: formData.isDefault,
|
||||
// steps: formData.steps.map(step => ({
|
||||
// ...step,
|
||||
// branchName: step.stepName, // branchName should be same as stepName
|
||||
// })),
|
||||
// clientSettings: hardcodedClientSettings
|
||||
// };
|
||||
|
||||
console.log("Update Data: ", updateData);
|
||||
|
||||
const response =
|
||||
await updateApprovalWorkflowWithClientSettings(updateData);
|
||||
const response = await updateApprovalWorkflowWithClientSettings(updateData);
|
||||
|
||||
console.log("Update Response: ", response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text:
|
||||
response?.message?.messages?.[0] ||
|
||||
"Failed to update approval workflow",
|
||||
text: response?.message?.messages?.[0] || "Failed to update approval workflow",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
|
|
@ -522,8 +403,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
// Call onSave to trigger parent refresh
|
||||
if (onSave) {
|
||||
|
|
@ -535,27 +416,24 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
// Create mode
|
||||
const submitData = {
|
||||
...formData,
|
||||
clientApprovalSettings: hardcodedClientSettings,
|
||||
clientApprovalSettings: hardcodedClientSettings
|
||||
};
|
||||
|
||||
console.log("Create Data: ", submitData);
|
||||
|
||||
const response =
|
||||
await createApprovalWorkflowWithClientSettings(submitData);
|
||||
const response = await createApprovalWorkflowWithClientSettings(submitData);
|
||||
|
||||
console.log("Create Response: ", response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text:
|
||||
response?.message?.messages?.[0] ||
|
||||
"Failed to create approval workflow",
|
||||
text: response?.message?.messages?.[0] || "Failed to create approval workflow",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
|
|
@ -564,8 +442,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
// Call onSave to trigger parent refresh
|
||||
if (onSave) {
|
||||
|
|
@ -582,8 +460,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -599,8 +477,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
confirmButtonText: "Yes, reset",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
setFormData({
|
||||
|
|
@ -638,15 +516,9 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="basic" disabled={isLoadingData}>
|
||||
Basic Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="steps" disabled={isLoadingData}>
|
||||
Workflow Steps
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" disabled={isLoadingData}>
|
||||
Client Settings
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
|
||||
<TabsTrigger value="steps" disabled={isLoadingData}>Workflow Steps</TabsTrigger>
|
||||
<TabsTrigger value="settings" disabled={isLoadingData}>Client Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information Tab */}
|
||||
|
|
@ -673,9 +545,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
type="textarea"
|
||||
placeholder="Describe the purpose and process of this workflow"
|
||||
value={formData.description}
|
||||
onChange={(value) =>
|
||||
handleBasicInfoChange("description", value)
|
||||
}
|
||||
onChange={(value) => handleBasicInfoChange("description", value)}
|
||||
error={errors.description}
|
||||
required
|
||||
rows={4}
|
||||
|
|
@ -695,9 +565,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
name="isDefault"
|
||||
type="checkbox"
|
||||
value={formData.isDefault}
|
||||
onChange={(value) =>
|
||||
handleBasicInfoChange("isDefault", value)
|
||||
}
|
||||
onChange={(value) => handleBasicInfoChange("isDefault", value)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
|
|
@ -705,9 +573,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
name="requiresApproval"
|
||||
type="checkbox"
|
||||
value={formData.requiresApproval}
|
||||
onChange={(value) =>
|
||||
handleBasicInfoChange("requiresApproval", value)
|
||||
}
|
||||
onChange={(value) => handleBasicInfoChange("requiresApproval", value)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
|
|
@ -715,9 +581,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
name="autoPublish"
|
||||
type="checkbox"
|
||||
value={formData.autoPublish}
|
||||
onChange={(value) =>
|
||||
handleBasicInfoChange("autoPublish", value)
|
||||
}
|
||||
onChange={(value) => handleBasicInfoChange("autoPublish", value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -731,22 +595,14 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Workflow Steps Configuration</span>
|
||||
<div className="text-sm text-gray-500">
|
||||
{formData.steps.length} step
|
||||
{formData.steps.length !== 1 ? "s" : ""}
|
||||
{formData.steps.length} step{formData.steps.length !== 1 ? 's' : ''}
|
||||
{(() => {
|
||||
const parallelGroups = formData.steps.reduce(
|
||||
(acc, step) => {
|
||||
acc[step.stepOrder] = (acc[step.stepOrder] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, number>,
|
||||
);
|
||||
const parallelCount = Object.values(parallelGroups).filter(
|
||||
(count) => count > 1,
|
||||
).length;
|
||||
return parallelCount > 0
|
||||
? ` • ${parallelCount} parallel group${parallelCount !== 1 ? "s" : ""}`
|
||||
: "";
|
||||
const parallelGroups = formData.steps.reduce((acc, step) => {
|
||||
acc[step.stepOrder] = (acc[step.stepOrder] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
const parallelCount = Object.values(parallelGroups).filter(count => count > 1).length;
|
||||
return parallelCount > 0 ? ` • ${parallelCount} parallel group${parallelCount !== 1 ? 's' : ''}` : '';
|
||||
})()}
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
|
@ -780,44 +636,21 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
<div className="h-12 w-12 mx-auto mb-2 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-400 text-xl">⚙️</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Settings Pre-configured
|
||||
</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Settings Pre-configured</h3>
|
||||
<p className="text-gray-600">
|
||||
Client approval settings are automatically configured with
|
||||
optimal defaults.
|
||||
Client approval settings are automatically configured with optimal defaults.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto">
|
||||
<h4 className="font-medium text-gray-900 mb-2">
|
||||
Default Settings:
|
||||
</h4>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Default Settings:</h4>
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
<li>
|
||||
• Requires Approval:{" "}
|
||||
<span className="font-medium text-green-600">Yes</span>
|
||||
</li>
|
||||
<li>
|
||||
• Auto Publish Articles:{" "}
|
||||
<span className="font-medium text-green-600">Yes</span>
|
||||
</li>
|
||||
<li>
|
||||
• Is Active:{" "}
|
||||
<span className="font-medium text-green-600">Yes</span>
|
||||
</li>
|
||||
<li>
|
||||
• Exempt Users:{" "}
|
||||
<span className="font-medium text-gray-500">None</span>
|
||||
</li>
|
||||
<li>
|
||||
• Exempt Roles:{" "}
|
||||
<span className="font-medium text-gray-500">None</span>
|
||||
</li>
|
||||
<li>
|
||||
• Exempt Categories:{" "}
|
||||
<span className="font-medium text-gray-500">None</span>
|
||||
</li>
|
||||
<li>• Requires Approval: <span className="font-medium text-green-600">Yes</span></li>
|
||||
<li>• Auto Publish Articles: <span className="font-medium text-green-600">Yes</span></li>
|
||||
<li>• Is Active: <span className="font-medium text-green-600">Yes</span></li>
|
||||
<li>• Exempt Users: <span className="font-medium text-gray-500">None</span></li>
|
||||
<li>• Exempt Roles: <span className="font-medium text-gray-500">None</span></li>
|
||||
<li>• Exempt Categories: <span className="font-medium text-gray-500">None</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -859,13 +692,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{isSubmitting
|
||||
? "Saving..."
|
||||
: isLoadingData
|
||||
? "Loading..."
|
||||
: workflowId
|
||||
? "Update Workflow"
|
||||
: "Save Workflow"}
|
||||
{isSubmitting ? "Saving..." : isLoadingData ? "Loading..." : workflowId ? "Update Workflow" : "Save Workflow"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,7 +8,6 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { formatDateToIndonesian } from "@/utils/globals";
|
||||
import { getArticleCategoryDetail } from "@/service/categories/categories";
|
||||
import { getUserInfo } from "@/service/user";
|
||||
|
||||
type CategoryDetail = {
|
||||
id: number;
|
||||
|
|
@ -26,32 +25,18 @@ type CategoryDetail = {
|
|||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdByFullname : string;
|
||||
};
|
||||
|
||||
type CurrentUser = {
|
||||
id: number;
|
||||
username: string;
|
||||
fullname?: string;
|
||||
};
|
||||
|
||||
export default function CategoriesDetailForm() {
|
||||
const { id } = useParams() as { id: string };
|
||||
const router = useRouter();
|
||||
const [detail, setDetail] = useState<CategoryDetail | null>(null);
|
||||
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getUserInfo().then((res) => setCurrentUser(res.data.data));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
if (id) {
|
||||
try {
|
||||
const res = await getArticleCategoryDetail(Number(id));
|
||||
const user = await getUserInfo();
|
||||
|
||||
setDetail(res?.data?.data);
|
||||
} catch (err) {
|
||||
console.error("Error fetching category detail:", err);
|
||||
|
|
@ -76,10 +61,10 @@ export default function CategoriesDetailForm() {
|
|||
</div>
|
||||
|
||||
{/* Slug */}
|
||||
{/* <div className="space-y-2 py-3">
|
||||
<div className="space-y-2 py-3">
|
||||
<Label>Slug</Label>
|
||||
<Input type="text" value={detail.slug || "-"} readOnly />
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2 py-3">
|
||||
|
|
@ -92,7 +77,7 @@ export default function CategoriesDetailForm() {
|
|||
</div>
|
||||
|
||||
{/* Thumbnail */}
|
||||
{/* <div className="space-y-2 py-3">
|
||||
<div className="space-y-2 py-3">
|
||||
<Label>Thumbnail</Label>
|
||||
<Card className="mt-2 w-fit p-2 border">
|
||||
<img
|
||||
|
|
@ -101,10 +86,10 @@ export default function CategoriesDetailForm() {
|
|||
className="h-[200px] rounded"
|
||||
/>
|
||||
</Card>
|
||||
</div> */}
|
||||
</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 p-4 border rounded-sm">
|
||||
{detail?.tags?.length > 0 ? (
|
||||
|
|
@ -117,7 +102,7 @@ export default function CategoriesDetailForm() {
|
|||
<span className="text-slate-400 text-sm">No tags</span>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* SIDEBAR */}
|
||||
|
|
@ -126,21 +111,19 @@ export default function CategoriesDetailForm() {
|
|||
{/* Creator */}
|
||||
<div>
|
||||
<Label>Created By</Label>
|
||||
<Input readOnly value={detail.createdByFullname || "-"} />
|
||||
|
||||
{/* <Input
|
||||
<Input
|
||||
type="text"
|
||||
value={detail.createdByName || detail.createdById}
|
||||
readOnly
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm text-green-500">
|
||||
{/* {detail.isPublish ? "Published" : "Draft"} |{" "} */}
|
||||
{detail.isActive ? "* Active" : "- Inactive"}
|
||||
<p className="text-sm text-slate-600">
|
||||
{detail.isPublish ? "Published" : "Draft"} |{" "}
|
||||
{detail.isActive ? "Active" : "Inactive"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ type CategoryDetail = {
|
|||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdByFullname: string;
|
||||
};
|
||||
|
||||
export default function CategoriesUpdateForm() {
|
||||
|
|
@ -61,7 +60,7 @@ export default function CategoriesUpdateForm() {
|
|||
}, [id]);
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => (prev ? { ...prev, [name]: value } : prev));
|
||||
|
|
@ -95,7 +94,7 @@ export default function CategoriesUpdateForm() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
res?.message || "Gagal mengunggah thumbnail",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -123,28 +122,11 @@ export default function CategoriesUpdateForm() {
|
|||
// 🔹 hanya kirim data mandatory
|
||||
const payload = {
|
||||
id: formData.id,
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
statusId: formData.statusId,
|
||||
|
||||
// ⬇️ FIELD KRUSIAL (WAJIB)
|
||||
isActive: true,
|
||||
is_active: true,
|
||||
isPublish: formData.isPublish,
|
||||
publishedAt: formData.publishedAt,
|
||||
parentId: formData.parentId,
|
||||
slug: formData.slug,
|
||||
createdById: formData.createdById,
|
||||
description: formData.description || "",
|
||||
statusId: formData.statusId || 1,
|
||||
title: formData.title || "",
|
||||
};
|
||||
|
||||
// const payload = {
|
||||
// id: formData.id,
|
||||
// description: formData.description || "",
|
||||
// statusId: formData.statusId || 1,
|
||||
// title: formData.title || "",
|
||||
// };
|
||||
console.log("UPDATE PAYLOAD:", payload);
|
||||
|
||||
const res = await updateArticleCategory(Number(id), payload);
|
||||
|
||||
if (!res?.error) {
|
||||
|
|
@ -204,7 +186,7 @@ export default function CategoriesUpdateForm() {
|
|||
</div>
|
||||
|
||||
{/* Thumbnail Upload */}
|
||||
{/* <div className="space-y-2 py-3">
|
||||
<div className="space-y-2 py-3">
|
||||
<Label>Thumbnail</Label>
|
||||
{thumbnailPreview && (
|
||||
<img
|
||||
|
|
@ -221,35 +203,28 @@ export default function CategoriesUpdateForm() {
|
|||
{isUploading && (
|
||||
<p className="text-sm text-blue-500 mt-1">Mengunggah...</p>
|
||||
)}
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <Checkbox
|
||||
<Checkbox
|
||||
checked={formData.isActive}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange("isActive", Boolean(checked))
|
||||
}
|
||||
/> */}
|
||||
{/* <input
|
||||
type="checkbox"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange("isActive", e.target.checked)
|
||||
}
|
||||
/>
|
||||
<Label>Active</Label> */}
|
||||
<Label>Active</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full mt-4 bg-[#1f2937] text-white py-2 rounded-xl hover:bg-slate-500"
|
||||
className="w-full mt-4 bg-green-600 text-white"
|
||||
disabled={isSubmitting || isUploading}
|
||||
>
|
||||
{isSubmitting ? "Menyimpan..." : "Simpan Perubahan"}
|
||||
</button>
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* SIDEBAR */}
|
||||
|
|
@ -258,9 +233,8 @@ export default function CategoriesUpdateForm() {
|
|||
<div>
|
||||
<Label>Created By</Label>
|
||||
<Input
|
||||
disabled
|
||||
type="text"
|
||||
value={formData.createdByFullname}
|
||||
value={formData.createdByName || formData.createdById}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,28 +3,13 @@ import React from "react";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
name: string;
|
||||
type?:
|
||||
| "text"
|
||||
| "email"
|
||||
| "password"
|
||||
| "number"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "textarea"
|
||||
| "select"
|
||||
| "checkbox";
|
||||
type?: "text" | "email" | "password" | "number" | "tel" | "url" | "textarea" | "select" | "checkbox";
|
||||
placeholder?: string;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
|
|
@ -83,9 +68,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|||
onValueChange={(val) => onChange(val)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`${hasError ? "border-red-500" : ""} ${className}`}
|
||||
>
|
||||
<SelectTrigger className={`${hasError ? "border-red-500" : ""} ${className}`}>
|
||||
<SelectValue placeholder={placeholder || `Select ${label}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -100,45 +83,20 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|||
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={value}
|
||||
onCheckedChange={(v) => onChange(!!v)}
|
||||
className="
|
||||
h-4 w-4
|
||||
rounded-full
|
||||
border border-black
|
||||
text-white
|
||||
data-[state=checked]:bg-black
|
||||
focus-visible:ring-2 focus-visible:ring-black
|
||||
"
|
||||
id={fieldId}
|
||||
checked={!!value}
|
||||
onCheckedChange={(checked) => onChange(checked)}
|
||||
disabled={disabled}
|
||||
className={hasError ? "border-red-500" : ""}
|
||||
/>
|
||||
|
||||
<Label
|
||||
htmlFor={fieldId}
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
<Label htmlFor={fieldId} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
// case "checkbox":
|
||||
// return (
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Checkbox
|
||||
// id={fieldId}
|
||||
// checked={!!value}
|
||||
// onCheckedChange={(checked) => onChange(checked)}
|
||||
// disabled={disabled}
|
||||
// className={hasError ? "border-red-500" : ""}
|
||||
// />
|
||||
// <Label htmlFor={fieldId} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
// {label}
|
||||
// </Label>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<Input
|
||||
|
|
@ -146,9 +104,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={value || ""}
|
||||
onChange={(e) =>
|
||||
onChange(e.target.value ? Number(e.target.value) : "")
|
||||
}
|
||||
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : "")}
|
||||
disabled={disabled}
|
||||
className={`${hasError ? "border-red-500" : ""} ${className}`}
|
||||
min={min}
|
||||
|
|
@ -183,9 +139,13 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|||
|
||||
{renderField()}
|
||||
|
||||
{helpText && <p className="text-xs text-gray-500">{helpText}</p>}
|
||||
{helpText && (
|
||||
<p className="text-xs text-gray-500">{helpText}</p>
|
||||
)}
|
||||
|
||||
{hasError && <p className="text-red-500 text-xs">{error}</p>}
|
||||
{hasError && (
|
||||
<p className="text-red-500 text-xs">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ 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" }),
|
||||
|
|
@ -94,7 +93,6 @@ type Detail = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -162,16 +160,6 @@ export default function FormVideoDetail() {
|
|||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDetail() {
|
||||
if (!id) return;
|
||||
|
|
@ -186,22 +174,9 @@ export default function FormVideoDetail() {
|
|||
uploadedById: details?.createdById,
|
||||
files: details?.files || [],
|
||||
thumbnailUrl: details?.thumbnailUrl || details?.thumbnail || "",
|
||||
publishedFor: details?.publishedFor,
|
||||
};
|
||||
setDetail(mappedDetail);
|
||||
setFiles(details?.files || []);
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
|
||||
setApproval(approvals?.data?.data);
|
||||
} catch (err) {
|
||||
|
|
@ -223,7 +198,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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -259,15 +234,6 @@ 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">
|
||||
|
|
@ -359,7 +325,7 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
|
||||
<div className="mt-3 px-3 space-y-2">
|
||||
<Label>Thumbnail</Label>
|
||||
<Label>Preview</Label>
|
||||
<Card className="mt-2 w-fit">
|
||||
<img
|
||||
src={detail.thumbnailUrl || detail.thumbnailLink}
|
||||
|
|
@ -384,64 +350,10 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* <div className="px-3 py-3 gap-2">
|
||||
<Label>Publish Target</Label>
|
||||
{[5, 6].map((target) => (
|
||||
<div key={target} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
id={String(target)}
|
||||
checked={selectedPublishers.includes(target)}
|
||||
onChange={() => handleCheckboxChange(target)}
|
||||
|
|
@ -451,7 +363,7 @@ export default function FormVideoDetail() {
|
|||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-3 border mx-3">
|
||||
<p>Information:</p>
|
||||
|
|
@ -475,46 +387,7 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* {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(detail.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail.isPublish === false && detail.statusId == 1)) &&
|
||||
Number(detail.uploadedById || detail.createdById) !=
|
||||
|
|
@ -542,7 +415,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">
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
interface FileWithPreview extends File {
|
||||
|
|
@ -412,11 +412,11 @@ export default function FormVideo() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
|
||||
const [publishedForError, setPublishedForError] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const userId = Cookies.get("userId");
|
||||
const [selectedSize, setSelectedSize] = useState("");
|
||||
|
|
@ -478,7 +478,7 @@ export default function FormVideo() {
|
|||
}
|
||||
|
||||
const filesWithPreview = acceptedFiles.map((file) =>
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) }),
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) })
|
||||
);
|
||||
|
||||
setFiles((prev) => {
|
||||
|
|
@ -503,12 +503,12 @@ export default function FormVideo() {
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["video/mp4", "video/mov", "video/avi"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
file.size <= 100 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .mp4, .mov, .avi, maksimal 100MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -722,7 +722,7 @@ export default function FormVideo() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
"",
|
||||
""
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -798,7 +798,7 @@ export default function FormVideo() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -829,7 +829,7 @@ export default function FormVideo() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id),
|
||||
.map((opt: any) => opt.id)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -866,8 +866,8 @@ export default function FormVideo() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
|
|
@ -930,11 +930,12 @@ export default function FormVideo() {
|
|||
}
|
||||
|
||||
if (id == undefined) {
|
||||
// New Articles API request data structure
|
||||
const articleData: CreateArticleData = {
|
||||
aiArticleId: 0,
|
||||
aiArticleId: 0, // default 0
|
||||
categoryIds: selectedCategory.toString(),
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
createdById: Number(userId), // isi dengan userId valid
|
||||
description: htmlToString(finalDescription),
|
||||
htmlDescription: finalDescription,
|
||||
isDraft: true,
|
||||
|
|
@ -947,9 +948,9 @@ export default function FormVideo() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 2,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// Use new Articles API
|
||||
const response = await createArticle(articleData);
|
||||
console.log("Article Data Submitted:", articleData);
|
||||
console.log("Article API Response:", response);
|
||||
|
|
@ -958,7 +959,7 @@ export default function FormVideo() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -986,7 +987,7 @@ export default function FormVideo() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1002,17 +1003,17 @@ export default function FormVideo() {
|
|||
try {
|
||||
const thumbnailResponse = await uploadArticleThumbnail(
|
||||
articleId,
|
||||
thumbnailFormData,
|
||||
thumbnailFormData
|
||||
);
|
||||
if (thumbnailResponse?.error) {
|
||||
console.warn(
|
||||
"Thumbnail upload failed:",
|
||||
thumbnailResponse.message,
|
||||
thumbnailResponse.message
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"Thumbnail uploaded successfully:",
|
||||
thumbnailResponse,
|
||||
thumbnailResponse
|
||||
);
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
|
|
@ -1024,7 +1025,7 @@ export default function FormVideo() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1075,7 +1076,7 @@ export default function FormVideo() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -1111,7 +1112,7 @@ export default function FormVideo() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1355,43 +1356,14 @@ export default function FormVideo() {
|
|||
<div className="flex flex-row items-center gap-3 py-2">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* <Switch
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
id="c2"
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setIsSwitchOn(checked)
|
||||
}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
|
|
@ -1650,13 +1622,14 @@ export default function FormVideo() {
|
|||
|
||||
<p className="text-sm font-semibold">Content Rewrite</p>
|
||||
<div className="my-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleRewriteClick}
|
||||
className="bg-blue-500 text-white py-2 px-4 rounded"
|
||||
>
|
||||
Content Rewrite
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showRewriteEditor && (
|
||||
|
|
@ -1841,7 +1814,7 @@ export default function FormVideo() {
|
|||
type="button"
|
||||
onClick={() => {
|
||||
const updatedTags = field.value.filter(
|
||||
(_, i) => i !== index,
|
||||
(_, i) => i !== index
|
||||
);
|
||||
field.onChange(updatedTags);
|
||||
}}
|
||||
|
|
@ -1879,19 +1852,19 @@ export default function FormVideo() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
const handleChange = () => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
.map((opt: any) => opt.id);
|
||||
} else {
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1902,47 +1875,17 @@ export default function FormVideo() {
|
|||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{/* <Checkbox
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/> */}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import { TimeIcon, TimesIcon } from "@/components/icons";
|
|||
|
||||
const CustomEditor = dynamic(
|
||||
() => import("@/components/editor/custom-editor"),
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const videoSchema = z.object({
|
||||
|
|
@ -59,12 +59,12 @@ const videoSchema = z.object({
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["video/mp4", "video/mov", "video/avi"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
file.size <= 100 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .mp4, .mov, .avi, maksimal 100MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
publishedFor: z
|
||||
.array(z.string())
|
||||
|
|
@ -129,7 +129,7 @@ export default function FormVideoUpdate() {
|
|||
}
|
||||
|
||||
const filesWithPreview = acceptedFiles.map((file) =>
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) }),
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) })
|
||||
);
|
||||
|
||||
setFiles((prev) => {
|
||||
|
|
@ -177,13 +177,7 @@ export default function FormVideoUpdate() {
|
|||
setSelectedCategory(String(detailData.categories[0].id));
|
||||
|
||||
setTags(detailData.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
// setPublishedFor(detailData.publishedFor?.split(",") || []);
|
||||
const publishTargets = detailData?.publishedFor
|
||||
? detailData.publishedFor.split(",")
|
||||
: [];
|
||||
|
||||
setPublishedFor(publishTargets);
|
||||
setValue("publishedFor", publishTargets);
|
||||
setPublishedFor(detailData.publishedFor?.split(",") || []);
|
||||
} catch (err) {
|
||||
close();
|
||||
console.error("❌ Error loading detail:", err);
|
||||
|
|
@ -207,7 +201,7 @@ export default function FormVideoUpdate() {
|
|||
|
||||
const handleCheckboxChange = (value: string) => {
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value],
|
||||
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -216,32 +210,20 @@ export default function FormVideoUpdate() {
|
|||
loading();
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
categoryIds: selectedCategory ?? "",
|
||||
createdById: detail?.createdById ?? "",
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
typeId: detail?.typeId ?? 2,
|
||||
};
|
||||
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory ?? "",
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 2,
|
||||
// };
|
||||
|
||||
console.log("📤 Payload Update:", payload);
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
|
|
@ -289,17 +271,6 @@ export default function FormVideoUpdate() {
|
|||
|
||||
if (!detail) return <p className="p-5 text-center">Memuat data...</p>;
|
||||
|
||||
const getVideoSrc = (url?: string) => {
|
||||
if (!url) return "";
|
||||
|
||||
// kalau sudah absolute
|
||||
if (url.startsWith("http")) return url;
|
||||
|
||||
// kalau backend kirim relative path
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
return `${baseUrl}${url}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
|
||||
|
|
@ -381,24 +352,21 @@ export default function FormVideoUpdate() {
|
|||
{detailFiles.map((file) => (
|
||||
<div key={file.id} className="flex flex-row items-center gap-4">
|
||||
<video
|
||||
className="object-contain w-[300px] h-[200px] rounded border"
|
||||
src={getVideoSrc(file.fileUrl)}
|
||||
className="object-contain w-[300px] h-[200px]"
|
||||
src={file.fileUrl}
|
||||
controls
|
||||
preload="metadata"
|
||||
title={file.fileName}
|
||||
/>
|
||||
|
||||
<p>{file.fileName}</p>
|
||||
<button
|
||||
type="button"
|
||||
<a
|
||||
className="text-destructive"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
>
|
||||
<TimesIcon />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{files?.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
|
|
@ -431,7 +399,7 @@ export default function FormVideoUpdate() {
|
|||
<Controller
|
||||
control={control}
|
||||
name="creatorName"
|
||||
render={({ field }) => <Input {...field} />}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
|
|
@ -487,68 +455,6 @@ export default function FormVideoUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const isAllChecked =
|
||||
field.value?.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: field.value?.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...(field.value || []), option.id]
|
||||
: field.value?.filter(
|
||||
(val) => val !== option.id,
|
||||
) || [];
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -564,19 +470,23 @@ export default function FormVideoUpdate() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
const handleChange = () => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
.map((opt: any) => opt.id);
|
||||
} else {
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
}
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
|
|
@ -588,11 +498,10 @@ export default function FormVideoUpdate() {
|
|||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
|
|
@ -608,23 +517,18 @@ export default function FormVideoUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-5">
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-black hover:bg-black hover:text-white rounded-lg px-8 py-4"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
<Button type="submit" className="border rounded-lg">Update</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="border border-black hover:bg-black hover:text-white rounded-lg px-8 py-4"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ 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" }),
|
||||
|
|
@ -84,75 +83,36 @@ type Category = {
|
|||
|
||||
type FileType = {
|
||||
id: number;
|
||||
secondaryUrl: string;
|
||||
thumbnailFileUrl: string;
|
||||
fileName: string;
|
||||
secondaryUrl?: string | null;
|
||||
fileUrl?: string | null;
|
||||
preview?: string;
|
||||
};
|
||||
|
||||
type Detail = {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
htmlDescription: string;
|
||||
slug: string;
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
typeId: number;
|
||||
tags: string;
|
||||
thumbnailUrl: string;
|
||||
pageUrl: string | null;
|
||||
createdById: number;
|
||||
createdByName: string;
|
||||
shareCount: number;
|
||||
viewCount: number;
|
||||
commentCount: number;
|
||||
aiArticleId: number | null;
|
||||
oldId: number;
|
||||
statusId: number;
|
||||
isBanner: boolean;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
slug: string | null;
|
||||
tags: string[];
|
||||
thumbnailPath: string | null;
|
||||
parentId: number;
|
||||
oldCategoryId: number | null;
|
||||
createdById: number;
|
||||
statusId: number;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isEnabled: boolean | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
// Legacy fields for backward compatibility
|
||||
category?: {
|
||||
category: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
creatorName?: string;
|
||||
thumbnailLink?: string;
|
||||
statusName?: string;
|
||||
needApprovalFromLevel?: number;
|
||||
uploadedById?: number;
|
||||
categoryName: string;
|
||||
creatorName: string;
|
||||
thumbnailLink: string;
|
||||
tags: string;
|
||||
statusName: string;
|
||||
isPublish: boolean;
|
||||
needApprovalFromLevel: number;
|
||||
files: FileType[];
|
||||
uploadedById: number;
|
||||
};
|
||||
|
||||
const ViewEditor = dynamic(
|
||||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormAudioDetail() {
|
||||
|
|
@ -257,19 +217,9 @@ export default function FormAudioDetail() {
|
|||
}
|
||||
}, [userLevelId, roleId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id: any) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -299,7 +249,9 @@ 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);
|
||||
|
|
@ -314,34 +266,14 @@ export default function FormAudioDetail() {
|
|||
});
|
||||
setupPlacementCheck(details?.files?.length);
|
||||
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
if (details?.publishedForObject) {
|
||||
const publisherIds = details?.publishedForObject.map(
|
||||
(obj: any) => obj.id,
|
||||
(obj: any) => obj.id
|
||||
);
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
const categoryId =
|
||||
details?.category?.id ||
|
||||
details?.categoryId ||
|
||||
details?.categories?.[0]?.id;
|
||||
|
||||
if (categoryId) {
|
||||
setSelectedTarget(String(categoryId));
|
||||
setSelectedCategory(String(categoryId));
|
||||
}
|
||||
setSelectedTarget(String(details.category.id));
|
||||
|
||||
const filesData = details?.files || [];
|
||||
// const audioFiles = filesData.filter(
|
||||
|
|
@ -450,7 +382,7 @@ export default function FormAudioDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean,
|
||||
checked: boolean
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -484,7 +416,7 @@ export default function FormAudioDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string,
|
||||
format: string
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -516,8 +448,6 @@ export default function FormAudioDetail() {
|
|||
});
|
||||
};
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
|
|
@ -587,47 +517,6 @@ export default function FormAudioDetail() {
|
|||
|
||||
<div className="w-full">
|
||||
<Label className="text-xl space-y-2">File Media</Label>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
{files.length === 0 ? (
|
||||
<p className="text-center text-gray-500">
|
||||
Tidak ada file media
|
||||
</p>
|
||||
) : (
|
||||
files.map((file, idx) => {
|
||||
const audioSrc = file.secondaryUrl || file.fileUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col gap-2 border p-2 rounded-md"
|
||||
>
|
||||
<p className="text-sm font-medium truncate">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
{audioSrc ? (
|
||||
<audio
|
||||
controls
|
||||
src={audioSrc}
|
||||
className="w-full rounded"
|
||||
>
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
) : (
|
||||
<p className="text-xs text-red-500">
|
||||
Audio source tidak tersedia
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="w-full">
|
||||
<Label className="text-xl space-y-2">File Media</Label>
|
||||
<div className="w-full">
|
||||
{files.length === 0 ? (
|
||||
<p className="text-center text-gray-500">
|
||||
|
|
@ -643,7 +532,7 @@ export default function FormAudioDetail() {
|
|||
))
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -671,58 +560,6 @@ export default function FormAudioDetail() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
|
@ -744,7 +581,7 @@ export default function FormAudioDetail() {
|
|||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<SuggestionModal
|
||||
id={Number(id)}
|
||||
numberOfSuggestion={detail?.numberOfSuggestion}
|
||||
|
|
@ -810,7 +647,7 @@ export default function FormAudioDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all",
|
||||
"all"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -827,7 +664,7 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes",
|
||||
"mabes"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "mabes", Boolean(e))
|
||||
|
|
@ -844,7 +681,7 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda",
|
||||
"polda"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "polda", Boolean(e))
|
||||
|
|
@ -862,13 +699,13 @@ export default function FormAudioDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international",
|
||||
"international"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e),
|
||||
Boolean(e)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -984,34 +821,30 @@ export default function FormAudioDetail() {
|
|||
Number(detail?.uploadedById) == 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>
|
||||
)}
|
||||
<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>
|
||||
{/* )
|
||||
) : (
|
||||
""
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormAudio() {
|
||||
|
|
@ -118,7 +118,7 @@ export default function FormAudio() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
|
||||
const [selectedSize, setSelectedSize] = useState("");
|
||||
|
|
@ -149,7 +149,7 @@ export default function FormAudio() {
|
|||
type FileWithPreview = File & { preview: string };
|
||||
const userId = Cookies.get("userId");
|
||||
|
||||
const options: Option[] = [
|
||||
const options: Option[] = [
|
||||
{ id: "all", label: "SEMUA" },
|
||||
{ id: "4", label: "UMUM" },
|
||||
{ id: "5", label: "JOURNALIS" },
|
||||
|
|
@ -181,7 +181,7 @@ export default function FormAudio() {
|
|||
}
|
||||
|
||||
const filesWithPreview = acceptedFiles.map((file) =>
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) }),
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) })
|
||||
);
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...filesWithPreview]);
|
||||
|
|
@ -216,11 +216,11 @@ export default function FormAudio() {
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["audio/mpeg", "audio/wav", "audio/mp3"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
file.size <= 100 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message: "Hanya file .mp3, .wav, maksimal 100MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -430,7 +430,7 @@ export default function FormAudio() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
"",
|
||||
""
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -504,7 +504,7 @@ export default function FormAudio() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -537,7 +537,7 @@ export default function FormAudio() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id),
|
||||
.map((opt: any) => opt.id)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -569,8 +569,8 @@ export default function FormAudio() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
return;
|
||||
|
|
@ -650,7 +650,6 @@ export default function FormAudio() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 4,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
|
||||
// Use new Articles API
|
||||
|
|
@ -662,7 +661,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -690,7 +689,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -707,19 +706,19 @@ export default function FormAudio() {
|
|||
try {
|
||||
const thumbnailResponse = await uploadArticleThumbnail(
|
||||
articleId,
|
||||
thumbnailFormData,
|
||||
thumbnailFormData
|
||||
);
|
||||
|
||||
if (thumbnailResponse?.error) {
|
||||
console.warn(
|
||||
"Thumbnail upload failed:",
|
||||
thumbnailResponse.message,
|
||||
thumbnailResponse.message
|
||||
);
|
||||
// Don't fail the whole process if thumbnail upload fails
|
||||
} else {
|
||||
console.log(
|
||||
"Thumbnail uploaded successfully:",
|
||||
thumbnailResponse,
|
||||
thumbnailResponse
|
||||
);
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
|
|
@ -732,7 +731,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -775,7 +774,7 @@ export default function FormAudio() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -811,7 +810,7 @@ export default function FormAudio() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1054,36 +1053,6 @@ export default function FormAudio() {
|
|||
<div className="flex flex-row items-center gap-3 py-2">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
|
|
@ -1092,7 +1061,7 @@ export default function FormAudio() {
|
|||
setIsSwitchOn(checked)
|
||||
}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
<div>
|
||||
|
|
@ -1595,19 +1564,19 @@ export default function FormAudio() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
const handleChange = () => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
.map((opt: any) => opt.id);
|
||||
} else {
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1618,47 +1587,17 @@ export default function FormAudio() {
|
|||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{/* <Checkbox
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/> */}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,12 +71,12 @@ const audioSchema = z.object({
|
|||
"audio/x-wav",
|
||||
"audio/mp4",
|
||||
"audio/aac",
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024,
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file audio (.mp3, .wav, .m4a, .aac) dengan ukuran maksimal 20MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
publishedFor: z
|
||||
|
|
@ -118,7 +118,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormAudioUpdate() {
|
||||
|
|
@ -232,7 +232,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
const handleEditTag = (index: number, newValue: string) => {
|
||||
setTags((prevTags) =>
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag)),
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -250,14 +250,6 @@ export default function FormAudioUpdate() {
|
|||
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
setPublishedFor(details.publishedFor?.split(",") || []);
|
||||
|
||||
if (details?.publishedFor) {
|
||||
const parsed = details.publishedFor
|
||||
.split(",")
|
||||
.map((id: string) => id.trim());
|
||||
|
||||
setValue("publishedFor", parsed); // 🔥 WAJIB
|
||||
}
|
||||
|
||||
if (details?.files) {
|
||||
setPrefFiles(details.files);
|
||||
// setFiles(details.files);
|
||||
|
|
@ -291,7 +283,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
const filesData = details.files || [];
|
||||
const fileUrls = filesData.map((file: { secondaryUrl: string }) =>
|
||||
file.secondaryUrl ? file.secondaryUrl : "default-image.jpg",
|
||||
file.secondaryUrl ? file.secondaryUrl : "default-image.jpg"
|
||||
);
|
||||
setDetailThumb(fileUrls);
|
||||
}
|
||||
|
|
@ -318,7 +310,7 @@ export default function FormAudioUpdate() {
|
|||
.filter(Boolean);
|
||||
|
||||
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
|
||||
options.includes(opt),
|
||||
options.includes(opt)
|
||||
);
|
||||
|
||||
return allSelected ? ["all", ...options] : options;
|
||||
|
|
@ -331,12 +323,12 @@ export default function FormAudioUpdate() {
|
|||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
);
|
||||
} else {
|
||||
// Toggle individual option
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -360,32 +352,20 @@ export default function FormAudioUpdate() {
|
|||
// isYoutube: false,
|
||||
// isInternationalMedia: false,
|
||||
// };
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
categoryIds: selectedCategory ?? "",
|
||||
createdById: detail?.createdById ?? "",
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
typeId: detail?.typeId ?? 4,
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory ?? "",
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 4,
|
||||
// };
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
if (res?.error) {
|
||||
|
|
@ -441,7 +421,7 @@ export default function FormAudioUpdate() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -478,7 +458,7 @@ export default function FormAudioUpdate() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -569,87 +549,50 @@ export default function FormAudioUpdate() {
|
|||
setFiles([...filtered]);
|
||||
};
|
||||
|
||||
const getAudioUrl = (file: any) => {
|
||||
if (file instanceof File) {
|
||||
return URL.createObjectURL(file);
|
||||
}
|
||||
return file.secondaryUrl || file.fileUrl || null;
|
||||
};
|
||||
|
||||
const fileList = files.map((file: any) => {
|
||||
const audioSrc = getAudioUrl(file);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id || file.name}
|
||||
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
|
||||
>
|
||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||
|
||||
{audioSrc && (
|
||||
<audio controls className="w-full">
|
||||
<source src={audioSrc} type={file.type || "audio/mpeg"} />
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="self-end"
|
||||
onClick={() => handleRemoveFile(file)}
|
||||
const fileList = files.map((file: any) => (
|
||||
<div
|
||||
key={file.id} // Gunakan ID file sebagai key
|
||||
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
/>
|
||||
</svg>{" "}
|
||||
<div>
|
||||
<div className="text-sm text-card-foreground">
|
||||
{file.fileName || file.name}
|
||||
</div>
|
||||
<div className="text-xs font-light text-muted-foreground">
|
||||
{Math.round(file.size / 100) / 10 > 1000 ? (
|
||||
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
|
||||
) : (
|
||||
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
|
||||
)}
|
||||
{" kb"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// const fileList = files.map((file: any) => (
|
||||
// <div
|
||||
// key={file.id} // Gunakan ID file sebagai key
|
||||
// className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
// >
|
||||
// <div className="flex gap-3 items-center">
|
||||
// <svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// width="48"
|
||||
// height="48"
|
||||
// viewBox="0 0 20 20"
|
||||
// >
|
||||
// <path
|
||||
// fill="currentColor"
|
||||
// d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
// />
|
||||
// </svg>{" "}
|
||||
// <div>
|
||||
// <div className="text-sm text-card-foreground">
|
||||
// {file.fileName || file.name}
|
||||
// </div>
|
||||
// <div className="text-xs font-light text-muted-foreground">
|
||||
// {Math.round(file.size / 100) / 10 > 1000 ? (
|
||||
// <>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
|
||||
// ) : (
|
||||
// <>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
|
||||
// )}
|
||||
// {" kb"}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <Button
|
||||
// type="button"
|
||||
// size="icon"
|
||||
// color="destructive"
|
||||
// variant="outline"
|
||||
// className="border-none rounded-full"
|
||||
// onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
|
||||
// >
|
||||
// <Icon icon="tabler:x" className="h-5 w-5" />
|
||||
// </Button>
|
||||
// </div>
|
||||
// ));
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
variant="outline"
|
||||
className="border-none rounded-full"
|
||||
onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
));
|
||||
|
||||
const handleCheckboxChangeImage = (fileId: number, value: string) => {
|
||||
setSelectedOptions((prev: any) => {
|
||||
|
|
@ -671,7 +614,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
// If all individual options are selected, include "all" automatically
|
||||
const isAllSelected = ["nasional", "wilayah", "internasional"].every(
|
||||
(opt) => updatedSelections.includes(opt),
|
||||
(opt) => updatedSelections.includes(opt)
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
|
|
@ -724,7 +667,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
// Jika berhasil, hapus file dari state lokal
|
||||
setFiles((prevFiles: any) =>
|
||||
prevFiles.filter((file: any) => file.id !== id),
|
||||
prevFiles.filter((file: any) => file.id !== id)
|
||||
);
|
||||
success();
|
||||
} catch (err) {
|
||||
|
|
@ -838,45 +781,9 @@ export default function FormAudioUpdate() {
|
|||
</Fragment>
|
||||
) : null}
|
||||
{prevFiles?.length > 0 &&
|
||||
prevFiles.map((file: any) => {
|
||||
const audioSrc = file.secondaryUrl || file.fileUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
|
||||
>
|
||||
<p className="text-sm font-medium truncate">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
{audioSrc ? (
|
||||
<audio controls className="w-full">
|
||||
<source src={audioSrc} type="audio/mpeg" />
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
) : (
|
||||
<p className="text-xs text-red-500">
|
||||
Audio source tidak tersedia
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="self-end"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
>
|
||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* {prevFiles?.length > 0 &&
|
||||
prevFiles.map((file: any) => (
|
||||
<div
|
||||
key={file.id}
|
||||
key={file.id} // Gunakan ID file sebagai key
|
||||
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
|
|
@ -905,7 +812,7 @@ export default function FormAudioUpdate() {
|
|||
) : (
|
||||
<>
|
||||
{(Math.round(file.size / 100) / 10).toFixed(
|
||||
1,
|
||||
1
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -924,7 +831,7 @@ export default function FormAudioUpdate() {
|
|||
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))} */}
|
||||
))}
|
||||
{/* {files.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label className="text-lg font-semibold">
|
||||
|
|
@ -1094,70 +1001,6 @@ export default function FormAudioUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const isAllChecked =
|
||||
field.value?.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: field.value?.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...(field.value || []), option.id]
|
||||
: field.value?.filter(
|
||||
(val) => val !== option.id,
|
||||
) || [];
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) =>
|
||||
handleChange(e.target.checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.name}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -1186,13 +1029,13 @@ export default function FormAudioUpdate() {
|
|||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter(
|
||||
(val) => val !== option.id,
|
||||
(val) => val !== option.id
|
||||
)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter(
|
||||
(val) => val !== "all",
|
||||
(val) => val !== "all"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1225,7 +1068,7 @@ export default function FormAudioUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ 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;
|
||||
|
|
@ -94,69 +93,30 @@ type FileType = {
|
|||
};
|
||||
|
||||
type Detail = {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
htmlDescription: string;
|
||||
slug: string;
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
typeId: number;
|
||||
tags: string;
|
||||
thumbnailUrl: string;
|
||||
pageUrl: string | null;
|
||||
createdById: number;
|
||||
createdByName: string;
|
||||
shareCount: number;
|
||||
viewCount: number;
|
||||
commentCount: number;
|
||||
aiArticleId: number | null;
|
||||
oldId: number;
|
||||
statusId: number;
|
||||
isBanner: boolean;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
publishedFor?: string | null;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
slug: string | null;
|
||||
tags: string[];
|
||||
thumbnailPath: string | null;
|
||||
parentId: number;
|
||||
oldCategoryId: number | null;
|
||||
createdById: number;
|
||||
statusId: number;
|
||||
isPublish: boolean;
|
||||
publishedAt: string | null;
|
||||
isEnabled: boolean | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}[];
|
||||
// Legacy fields for backward compatibility
|
||||
category?: {
|
||||
category: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
creatorName?: string;
|
||||
thumbnailLink?: string;
|
||||
statusName?: string;
|
||||
needApprovalFromLevel?: number;
|
||||
uploadedById?: number;
|
||||
categoryName: string;
|
||||
creatorName: string;
|
||||
thumbnailLink: string;
|
||||
tags: string;
|
||||
statusName: string;
|
||||
isPublish: boolean;
|
||||
needApprovalFromLevel: number;
|
||||
files: FileType[];
|
||||
uploadedById: number;
|
||||
};
|
||||
|
||||
const ViewEditor = dynamic(
|
||||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormTeksDetail() {
|
||||
|
|
@ -235,7 +195,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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -247,16 +207,6 @@ export default function FormTeksDetail() {
|
|||
initState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail?.publishedFor) return;
|
||||
|
||||
const publisherIds = detail.publishedFor
|
||||
.split(",")
|
||||
.map((id: any) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}, [detail]);
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const categoryRes = await listArticleCategories(1, 100);
|
||||
|
|
@ -293,18 +243,6 @@ export default function FormTeksDetail() {
|
|||
setFiles(details?.files || []);
|
||||
setDetail(details);
|
||||
|
||||
// 🔥 Parse publish target seperti content image
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
// ✅ Aman untuk fileType
|
||||
setMain({
|
||||
type: details?.fileType?.name || "Unknown",
|
||||
|
|
@ -315,7 +253,7 @@ export default function FormTeksDetail() {
|
|||
|
||||
if (details?.publishedForObject) {
|
||||
const publisherIds = details.publishedForObject.map(
|
||||
(obj: any) => obj.id,
|
||||
(obj: any) => obj.id
|
||||
);
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
|
@ -421,7 +359,7 @@ export default function FormTeksDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean,
|
||||
checked: boolean
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -462,7 +400,7 @@ export default function FormTeksDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string,
|
||||
format: string
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -486,8 +424,6 @@ export default function FormTeksDetail() {
|
|||
});
|
||||
};
|
||||
|
||||
const isPending = Number(detail?.statusId) === 1;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{detail !== undefined ? (
|
||||
|
|
@ -592,10 +528,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">
|
||||
|
|
@ -624,58 +560,6 @@ export default function FormTeksDetail() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="5"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
|
|
@ -697,7 +581,7 @@ export default function FormTeksDetail() {
|
|||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<SuggestionModal
|
||||
id={Number(id)}
|
||||
numberOfSuggestion={detail?.numberOfSuggestion}
|
||||
|
|
@ -763,7 +647,7 @@ export default function FormTeksDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all",
|
||||
"all"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -780,7 +664,7 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes",
|
||||
"mabes"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "mabes", Boolean(e))
|
||||
|
|
@ -797,7 +681,7 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda",
|
||||
"polda"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "polda", Boolean(e))
|
||||
|
|
@ -815,13 +699,13 @@ export default function FormTeksDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international",
|
||||
"international"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e),
|
||||
Boolean(e)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -933,17 +817,10 @@ export default function FormTeksDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
{/* {isPending &&
|
||||
(Number(detail?.needApprovalFromLevel || 0) ===
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia === true &&
|
||||
detail?.isForwardFromNational === true)) ? (
|
||||
Number(detail?.createdById || detail?.uploadedById) ==
|
||||
Number(userId) ? (
|
||||
{/* {Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
|
||||
Number(detail?.uploadedById) == Number(userId) ? (
|
||||
""
|
||||
) : ( */}
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
|
|
@ -952,7 +829,6 @@ 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"
|
||||
|
|
@ -960,7 +836,6 @@ export default function FormTeksDetail() {
|
|||
>
|
||||
<Icon icon="fa:comment-o" className="mr-3" /> Revision
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => actionApproval("4")}
|
||||
color="destructive"
|
||||
|
|
@ -970,9 +845,7 @@ export default function FormTeksDetail() {
|
|||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
)}
|
||||
{/* )
|
||||
{/* )
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
type Option = {
|
||||
|
|
@ -114,7 +114,7 @@ export default function FormTeks() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [isContentRewriteClicked, setIsContentRewriteClicked] = useState(false);
|
||||
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
|
||||
|
|
@ -168,13 +168,13 @@ export default function FormTeks() {
|
|||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024,
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024
|
||||
);
|
||||
|
||||
const filesWithPreview = filtered.map((file) =>
|
||||
Object.assign(file, {
|
||||
preview: URL.createObjectURL(file),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
setFiles(filesWithPreview);
|
||||
|
|
@ -200,12 +200,12 @@ export default function FormTeks() {
|
|||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
].includes(file.type) && file.size <= 100 * 1024 * 1024,
|
||||
].includes(file.type) && file.size <= 100 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .pdf, .doc, .docx, .txt, maksimal 100MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -420,7 +420,7 @@ export default function FormTeks() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
"",
|
||||
""
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -500,7 +500,7 @@ export default function FormTeks() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -533,7 +533,7 @@ export default function FormTeks() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id),
|
||||
.map((opt: any) => opt.id)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -572,8 +572,8 @@ export default function FormTeks() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
|
|
@ -653,7 +653,6 @@ export default function FormTeks() {
|
|||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 3,
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
};
|
||||
const response = await createArticle(articleData);
|
||||
console.log("Article Data Submitted:", articleData);
|
||||
|
|
@ -663,7 +662,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -687,7 +686,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -703,18 +702,18 @@ export default function FormTeks() {
|
|||
try {
|
||||
const thumbnailResponse = await uploadArticleThumbnail(
|
||||
articleId,
|
||||
thumbnailFormData,
|
||||
thumbnailFormData
|
||||
);
|
||||
|
||||
if (thumbnailResponse?.error) {
|
||||
console.warn(
|
||||
"Thumbnail upload failed:",
|
||||
thumbnailResponse.message,
|
||||
thumbnailResponse.message
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"Thumbnail uploaded successfully:",
|
||||
thumbnailResponse,
|
||||
thumbnailResponse
|
||||
);
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
|
|
@ -726,7 +725,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -768,7 +767,7 @@ export default function FormTeks() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -805,7 +804,7 @@ export default function FormTeks() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1037,36 +1036,6 @@ export default function FormTeks() {
|
|||
<div className="flex flex-row items-center gap-3 py-2">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
|
|
@ -1075,7 +1044,7 @@ export default function FormTeks() {
|
|||
setIsSwitchOn(checked)
|
||||
}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
<div>
|
||||
|
|
@ -1548,42 +1517,19 @@ export default function FormTeks() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
const handleChange = () => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
.map((opt: any) => opt.id);
|
||||
} else {
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1599,19 +1545,12 @@ export default function FormTeks() {
|
|||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{/* <Checkbox
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/> */}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import * as z from "zod";
|
|||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -71,12 +70,12 @@ const teksSchema = z.object({
|
|||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
].includes(file.type) && file.size <= 10 * 1024 * 1024,
|
||||
].includes(file.type) && file.size <= 10 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .pdf, .doc, .docx, atau .txt dengan ukuran maksimal 10MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
publishedFor: z
|
||||
|
|
@ -89,12 +88,6 @@ type Category = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
interface DetailFile {
|
||||
id: number;
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
type Detail = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -117,7 +110,6 @@ type Detail = {
|
|||
|
||||
interface FileWithPreview extends File {
|
||||
preview: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Option = {
|
||||
|
|
@ -129,7 +121,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormTeksUpdate() {
|
||||
|
|
@ -161,7 +153,6 @@ export default function FormTeksUpdate() {
|
|||
[fileId: number]: string[];
|
||||
}>({});
|
||||
const [selectedTarget, setSelectedTarget] = useState("");
|
||||
const [detailFiles, setDetailFiles] = useState<DetailFile[]>([]);
|
||||
const [unitSelection, setUnitSelection] = useState({
|
||||
allUnit: false,
|
||||
mabes: false,
|
||||
|
|
@ -170,26 +161,18 @@ export default function FormTeksUpdate() {
|
|||
});
|
||||
const [publishedFor, setPublishedFor] = useState<string[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [existingFiles, setExistingFiles] = useState<DetailFile[]>([]);
|
||||
|
||||
let fileTypeId = "3";
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFiles(
|
||||
acceptedFiles.map((file) =>
|
||||
Object.assign(file, {
|
||||
id: uuidv4(),
|
||||
preview: URL.createObjectURL(file),
|
||||
}),
|
||||
),
|
||||
);
|
||||
setFiles(acceptedFiles.map((file) => Object.assign(file)));
|
||||
},
|
||||
accept: {
|
||||
"application/pdf": [],
|
||||
"application/msword": [],
|
||||
"application/msword": [], // .doc
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
[],
|
||||
[], // .docx
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -236,15 +219,9 @@ export default function FormTeksUpdate() {
|
|||
setValue("creatorName", details.createdByName ?? "");
|
||||
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
|
||||
setPublishedFor(details.publishedFor?.split(",") || []);
|
||||
if (details?.publishedFor) {
|
||||
const publishArr = details.publishedFor.split(",");
|
||||
|
||||
setPublishedFor(publishArr); // state lokal
|
||||
setValue("publishedFor", publishArr); // ← WAJIB untuk react-hook-form
|
||||
}
|
||||
|
||||
if (details?.files) {
|
||||
setExistingFiles(details.files);
|
||||
setFiles(details.files);
|
||||
const initialOptions: { [key: number]: string[] } = {};
|
||||
details.files.forEach((file: any) => {
|
||||
if (file.placements) {
|
||||
|
|
@ -296,11 +273,12 @@ export default function FormTeksUpdate() {
|
|||
.filter(Boolean);
|
||||
|
||||
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
|
||||
options.includes(opt),
|
||||
options.includes(opt)
|
||||
);
|
||||
|
||||
return allSelected ? ["all", ...options] : options;
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (id: string) => {
|
||||
if (id === "all") {
|
||||
// Select all options except "all"
|
||||
|
|
@ -308,12 +286,12 @@ export default function FormTeksUpdate() {
|
|||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
);
|
||||
} else {
|
||||
// Toggle individual option
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -338,32 +316,20 @@ export default function FormTeksUpdate() {
|
|||
// isYoutube: false,
|
||||
// isInternationalMedia: false,
|
||||
// };
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail.aiArticleId,
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
categoryIds: selectedCategory,
|
||||
createdById: detail?.createdById ?? "",
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
typeId: detail?.typeId ?? 3,
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory,
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 3,
|
||||
// };
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
if (res?.error) {
|
||||
|
|
@ -420,7 +386,7 @@ export default function FormTeksUpdate() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -458,7 +424,7 @@ export default function FormTeksUpdate() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -602,7 +568,7 @@ export default function FormTeksUpdate() {
|
|||
|
||||
// If all individual options are selected, include "all" automatically
|
||||
const isAllSelected = ["nasional", "wilayah", "internasional"].every(
|
||||
(opt) => updatedSelections.includes(opt),
|
||||
(opt) => updatedSelections.includes(opt)
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
|
|
@ -633,7 +599,7 @@ export default function FormTeksUpdate() {
|
|||
|
||||
const handleEditTag = (index: number, newValue: string) => {
|
||||
setTags((prevTags) =>
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag)),
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -702,142 +668,151 @@ export default function FormTeksUpdate() {
|
|||
)}
|
||||
</div>
|
||||
<div className="py-3 space-y-2">
|
||||
<Label>Select File</Label>
|
||||
{/* <Input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
/> */}
|
||||
<Fragment>
|
||||
<div className="py-3 space-y-2">
|
||||
<Label>Select File</Label>
|
||||
|
||||
<Fragment>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className="w-full text-center border-dashed border border-black rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUpload className="w-10 h-10" />
|
||||
<h4 className="text-2xl font-medium mt-3">
|
||||
Drag File
|
||||
</h4>
|
||||
</div>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUpload className="text-default-300 w-10 h-10" />
|
||||
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
|
||||
{/* Drop files here or click to upload. */}
|
||||
Drag File
|
||||
</h4>
|
||||
<div className=" text-xs text-muted-foreground">
|
||||
Upload File Text Max
|
||||
</div>
|
||||
|
||||
{/* 👇 TARUH DI SINI */}
|
||||
{files.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-lg font-semibold">
|
||||
File Baru
|
||||
</Label>
|
||||
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className="flex justify-between items-center border p-3 rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setFiles((prev) =>
|
||||
prev.filter(
|
||||
(item) => item.name !== file.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* {files.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label className="text-md font-semibold">
|
||||
{files.length ? (
|
||||
<Fragment>
|
||||
<div>{fileList}</div>
|
||||
<div className=" flex justify-between gap-2">
|
||||
<div className="flex flex-row items-center gap-3 py-3">
|
||||
<Label>Watermark</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch defaultChecked color="primary" id="c2" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
color="destructive"
|
||||
onClick={handleRemoveAllFiles}
|
||||
>
|
||||
Remove All
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : null}
|
||||
{files.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-lg font-semibold">
|
||||
{" "}
|
||||
File Media
|
||||
</Label>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{files.map((file: any, index: number) => (
|
||||
{files.map((file: any) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center border p-2 rounded-md"
|
||||
>
|
||||
{file.preview ? (
|
||||
<img
|
||||
src={file.preview}
|
||||
alt={file.name}
|
||||
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-16 h-16"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-grow">
|
||||
<p className="font-medium">
|
||||
{file.fileName || file.name}
|
||||
</p>
|
||||
<img
|
||||
src={file.thumbnailFileUrl}
|
||||
alt={file.fileName}
|
||||
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-3 items-center ">
|
||||
<div className="flex-grow">
|
||||
<p className="font-medium">{file.fileName}</p>
|
||||
<a
|
||||
href={file.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 text-sm"
|
||||
>
|
||||
View File
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedOptions[
|
||||
file.id
|
||||
]?.includes("all")}
|
||||
onChange={() =>
|
||||
handleCheckboxChangeImage(
|
||||
file.id,
|
||||
"all"
|
||||
)
|
||||
}
|
||||
className="form-checkbox"
|
||||
/>
|
||||
<span>All</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedOptions[
|
||||
file.id
|
||||
]?.includes("nasional")}
|
||||
onChange={() =>
|
||||
handleCheckboxChangeImage(
|
||||
file.id,
|
||||
"nasional"
|
||||
)
|
||||
}
|
||||
className="form-checkbox"
|
||||
/>
|
||||
<span>Nasional</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedOptions[
|
||||
file.id
|
||||
]?.includes("wilayah")}
|
||||
onChange={() =>
|
||||
handleCheckboxChangeImage(
|
||||
file.id,
|
||||
"wilayah"
|
||||
)
|
||||
}
|
||||
className="form-checkbox"
|
||||
/>
|
||||
<span>Wilayah</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedOptions[
|
||||
file.id
|
||||
]?.includes("internasional")}
|
||||
onChange={() =>
|
||||
handleCheckboxChangeImage(
|
||||
file.id,
|
||||
"internasional"
|
||||
)
|
||||
}
|
||||
className="form-checkbox"
|
||||
/>
|
||||
<span>Internasional</span>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setFiles((prev) =>
|
||||
prev.filter((f) => f.id !== file.id),
|
||||
)
|
||||
}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Existing Files */}
|
||||
{existingFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-lg font-semibold">
|
||||
File Sebelumnya
|
||||
</Label>
|
||||
|
||||
{existingFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex justify-between items-center border p-3 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-10 h-10"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium">{file.fileName}</p>
|
||||
<a
|
||||
href={file.fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 text-sm"
|
||||
>
|
||||
Lihat File
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
|
|
@ -865,18 +840,11 @@ export default function FormTeksUpdate() {
|
|||
{/* <div className="mt-3 px-3">
|
||||
<Label>Pratinjau Gambar Utama</Label>
|
||||
<Card className="mt-2">
|
||||
{files.preview ? (
|
||||
<img
|
||||
src={files.preview}
|
||||
alt={files.name}
|
||||
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="tabler:file-description"
|
||||
className="w-16 h-16"
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={detail.thumbnailLink}
|
||||
alt="Thumbnail Gambar Utama"
|
||||
className="w-full h-auto rounded"
|
||||
/>
|
||||
</Card>
|
||||
</div> */}
|
||||
<div className="px-3 py-3">
|
||||
|
|
@ -918,76 +886,6 @@ export default function FormTeksUpdate() {
|
|||
<div className="mt-4 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => {
|
||||
const currentValue = field.value || [];
|
||||
|
||||
const isAllChecked =
|
||||
currentValue.length ===
|
||||
options.filter((opt) => opt.id !== "all").length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{options.map((option) => {
|
||||
const isChecked =
|
||||
option.id === "all"
|
||||
? isAllChecked
|
||||
: currentValue.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = checked
|
||||
? [...currentValue, option.id]
|
||||
: currentValue.filter(
|
||||
(val) => val !== option.id,
|
||||
);
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) =>
|
||||
handleChange(e.target.checked)
|
||||
}
|
||||
/>
|
||||
|
||||
<Label htmlFor={option.id}>
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{errors.publishedFor && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{errors.publishedFor.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="publishedFor"
|
||||
render={({ field }) => (
|
||||
|
|
@ -1016,13 +914,13 @@ export default function FormTeksUpdate() {
|
|||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter(
|
||||
(val) => val !== option.id,
|
||||
(val) => val !== option.id
|
||||
)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter(
|
||||
(val) => val !== "all",
|
||||
(val) => val !== "all"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1057,7 +955,7 @@ export default function FormTeksUpdate() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ 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" }),
|
||||
|
|
@ -127,7 +126,6 @@ type Detail = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: FileType[] | null;
|
||||
published_for?: string;
|
||||
categories: {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -163,7 +161,7 @@ const ViewEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/view-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormImageDetail() {
|
||||
|
|
@ -173,8 +171,10 @@ 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 +233,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 +255,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) {
|
||||
|
|
@ -323,22 +323,12 @@ export default function FormImageDetail() {
|
|||
try {
|
||||
const response = await getArticleDetail(Number(id));
|
||||
const details = response?.data?.data;
|
||||
console.log("detail", details);
|
||||
|
||||
console.log("DETAIL RESPONSE:", details);
|
||||
// ===== PARSE published_for =====
|
||||
const rawPublished =
|
||||
details?.published_for || details?.publishedFor || "";
|
||||
|
||||
if (rawPublished) {
|
||||
const publisherIds = rawPublished
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
// Map the new API response to the expected format
|
||||
const mappedDetail: Detail = {
|
||||
...details,
|
||||
// Map legacy fields for backward compatibility
|
||||
category:
|
||||
details.categories && details.categories.length > 0
|
||||
? {
|
||||
|
|
@ -349,34 +339,28 @@ export default function FormImageDetail() {
|
|||
creatorName: details.createdByName,
|
||||
thumbnailLink: details.thumbnailUrl,
|
||||
statusName: getStatusName(details.statusId),
|
||||
needApprovalFromLevel: 0,
|
||||
needApprovalFromLevel: 0, // This might need to be updated based on your business logic
|
||||
uploadedById: details.createdById,
|
||||
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,
|
||||
fileName: file.fileName || file.fileName,
|
||||
// Keep original API fields for reference
|
||||
...file,
|
||||
}));
|
||||
|
||||
setFiles(mappedFiles);
|
||||
setDetail(mappedDetail);
|
||||
|
||||
if (details?.published_for) {
|
||||
const publisherIds = details.published_for
|
||||
.split(",")
|
||||
.map((id: string) => Number(id.trim()));
|
||||
|
||||
setSelectedPublishers(publisherIds);
|
||||
}
|
||||
|
||||
if (mappedFiles && mappedFiles.length > 0) {
|
||||
setMain({
|
||||
type: "image",
|
||||
type: "image", // Default type for articles
|
||||
url: mappedFiles[0]?.url || mappedDetail.thumbnailUrl,
|
||||
names: mappedFiles[0]?.fileName || "image",
|
||||
format: getFileExtension(mappedFiles[0]?.fileName || "jpg"),
|
||||
|
|
@ -384,22 +368,19 @@ export default function FormImageDetail() {
|
|||
setupPlacementCheck(mappedFiles.length);
|
||||
}
|
||||
|
||||
// Set the selected target to the category ID from details
|
||||
setSelectedTarget(String(mappedDetail.categoryId));
|
||||
|
||||
const fileUrls = (mappedFiles || []).map(
|
||||
(file) => file.thumbnailFileUrl || file.url || "default-image.jpg",
|
||||
const fileUrls = mappedFiles.map(
|
||||
(file: any) =>
|
||||
file.thumbnailFileUrl ||
|
||||
file.url ||
|
||||
mappedDetail.thumbnailUrl ||
|
||||
"default-image.jpg"
|
||||
);
|
||||
|
||||
setDetailThumb(fileUrls);
|
||||
|
||||
// if (details?.publishedForObject?.length > 0) {
|
||||
// const publisherIds = details.publishedForObject
|
||||
// .map((obj: any) => Number(obj.id))
|
||||
// .filter((id: number) => id === 4 || id === 5);
|
||||
|
||||
// setSelectedPublishers(publisherIds);
|
||||
// }
|
||||
|
||||
// Note: You might need to update this API call as well
|
||||
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
|
||||
setApproval(approvals?.data?.data);
|
||||
} catch (error) {
|
||||
|
|
@ -410,6 +391,7 @@ export default function FormImageDetail() {
|
|||
initState();
|
||||
}, [refresh, setValue]);
|
||||
|
||||
// Helper function to get status name from status ID
|
||||
const getStatusName = (statusId: number): string => {
|
||||
const statusMap: { [key: number]: string } = {
|
||||
1: "Menunggu Review",
|
||||
|
|
@ -500,7 +482,7 @@ export default function FormImageDetail() {
|
|||
const setupPlacement = (
|
||||
index: number,
|
||||
placement: string,
|
||||
checked: boolean,
|
||||
checked: boolean
|
||||
) => {
|
||||
let temp = [...filePlacements];
|
||||
if (checked) {
|
||||
|
|
@ -541,7 +523,7 @@ export default function FormImageDetail() {
|
|||
type: string,
|
||||
url: string,
|
||||
names: string,
|
||||
format: string,
|
||||
format: string
|
||||
) => {
|
||||
console.log("Test 3 :", type, url, names, format);
|
||||
setMain({
|
||||
|
|
@ -588,15 +570,6 @@ 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 ? (
|
||||
|
|
@ -646,14 +619,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}
|
||||
|
|
@ -696,7 +669,7 @@ export default function FormImageDetail() {
|
|||
navigation={false}
|
||||
className="h-[480px] object-cover w-full"
|
||||
>
|
||||
{/* {detailThumb?.map((data: any, index: number) => (
|
||||
{detailThumb?.map((data: any, index: number) => (
|
||||
<SwiperSlide key={index}>
|
||||
<img
|
||||
className="h-[480px] max-w-[600px] rounded-md object-cover mx-auto border-2"
|
||||
|
|
@ -704,15 +677,6 @@ export default function FormImageDetail() {
|
|||
alt={`Image ${index + 1}`}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))} */}
|
||||
{detailThumb?.map((url: string, index: number) => (
|
||||
<SwiperSlide key={index}>
|
||||
<img
|
||||
src={url}
|
||||
alt={`Image ${index + 1}`}
|
||||
className="h-[480px] max-w-[600px] rounded-md object-cover mx-auto border-2"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
<div className="mt-2 mx-auto min-w-fit max-w-[600px]">
|
||||
|
|
@ -796,54 +760,26 @@ export default function FormImageDetail() {
|
|||
<div className="px-3 py-3">
|
||||
<div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
|
||||
{/* UMUM = 4 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="4"
|
||||
value="4"
|
||||
checked={selectedPublishers.includes(4)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
|
||||
{/* JOURNALIS = 5 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="5"
|
||||
value="5"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
readOnly
|
||||
className="h-4 w-4 border border-gray-300 rounded"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-2 space-y-2">
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="4"
|
||||
checked={selectedPublishers.includes(5)}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Label htmlFor="4">UMUM</Label>
|
||||
</div>
|
||||
<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>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="6"
|
||||
checked={selectedPublishers.includes(6)}
|
||||
disabled
|
||||
onChange={() => handleCheckboxChange(6)}
|
||||
className="border"
|
||||
/>
|
||||
<Label htmlFor="5">JOURNALIS</Label>
|
||||
<Label htmlFor="6">JOURNALIS</Label>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SuggestionModal
|
||||
|
|
@ -911,7 +847,7 @@ export default function FormImageDetail() {
|
|||
)} */}
|
||||
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="max-h-[600px] w-full">
|
||||
<DialogContent className="max-h-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Leave Comment</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
@ -923,7 +859,7 @@ export default function FormImageDetail() {
|
|||
className="flex flex-row gap-5 items-center w-full"
|
||||
>
|
||||
<div className="w-[200px] h-[100px] flex justify-center items-center">
|
||||
{/* <img
|
||||
<img
|
||||
key={index}
|
||||
alt={file.fileAlt || `files-${index + 1}`}
|
||||
src={file.url}
|
||||
|
|
@ -931,19 +867,6 @@ export default function FormImageDetail() {
|
|||
className={`h-[100px] object-cover ${
|
||||
portraitMap[index] ? "w-auto" : "!w-[200px]"
|
||||
}`}
|
||||
/> */}
|
||||
<img
|
||||
alt={file.fileAlt || `files-${index + 1}`}
|
||||
src={
|
||||
file.fileUrl ||
|
||||
file.url ||
|
||||
file.fileThumbnail ||
|
||||
file.thumbnailFileUrl
|
||||
}
|
||||
onLoad={(e) => handleImageLoad(e, index)}
|
||||
className={`h-[100px] object-contain ${
|
||||
portraitMap[index] ? "w-auto" : "w-[200px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
|
|
@ -964,7 +887,7 @@ export default function FormImageDetail() {
|
|||
id="terms"
|
||||
value="all"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"all",
|
||||
"all"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(index, "all", Boolean(e))
|
||||
|
|
@ -981,13 +904,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"mabes",
|
||||
"mabes"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"mabes",
|
||||
Boolean(e),
|
||||
Boolean(e)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -1002,13 +925,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"polda",
|
||||
"polda"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"polda",
|
||||
Boolean(e),
|
||||
Boolean(e)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -1033,13 +956,13 @@ export default function FormImageDetail() {
|
|||
<Checkbox
|
||||
id="terms"
|
||||
checked={filePlacements[index]?.includes(
|
||||
"international",
|
||||
"international"
|
||||
)}
|
||||
onCheckedChange={(e) =>
|
||||
setupPlacement(
|
||||
index,
|
||||
"international",
|
||||
Boolean(e),
|
||||
Boolean(e)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
@ -1160,53 +1083,15 @@ export default function FormImageDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
|
||||
{/* {(Number(detail.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail.isPublish === false && detail.statusId == 1)) &&
|
||||
Number(detail.uploadedById || detail.createdById) !=
|
||||
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>
|
||||
) : null} */}
|
||||
|
||||
{/* {Number(detail?.needApprovalFromLevel || 0) ==
|
||||
{Number(detail?.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia == true &&
|
||||
detail?.isForwardFromNational == true &&
|
||||
Number(detail?.statusId) == 1) ? ( */}
|
||||
{/* {isPending &&
|
||||
(Number(detail?.needApprovalFromLevel || 0) ===
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia === true &&
|
||||
detail?.isForwardFromNational === true)) ? (
|
||||
Number(detail?.statusId) == 1) ? (
|
||||
Number(detail?.createdById || detail?.uploadedById) ==
|
||||
Number(userId) ? (
|
||||
""
|
||||
) : ( */}
|
||||
{isPending && (
|
||||
<AccessGuard action="approve">
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Button
|
||||
onClick={() => actionApproval("2")}
|
||||
|
|
@ -1232,12 +1117,10 @@ export default function FormImageDetail() {
|
|||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</AccessGuard>
|
||||
)}
|
||||
{/* )
|
||||
)
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormImage() {
|
||||
|
|
@ -113,7 +113,7 @@ export default function FormImage() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
|
||||
const [selectedWritingStyle, setSelectedWritingStyle] =
|
||||
|
|
@ -169,17 +169,17 @@ export default function FormImage() {
|
|||
.filter(
|
||||
(file) =>
|
||||
["image/jpeg", "image/png", "image/jpg"].includes(file.type) &&
|
||||
file.size <= MAX_FILE_SIZE,
|
||||
file.size <= MAX_FILE_SIZE
|
||||
)
|
||||
.map((file) =>
|
||||
Object.assign(file, {
|
||||
preview: URL.createObjectURL(file),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
toast.error(
|
||||
"File tidak valid. Hanya .jpg, .jpeg, .png maksimal 100MB yang diperbolehkan.",
|
||||
"File tidak valid. Hanya .jpg, .jpeg, .png maksimal 100MB yang diperbolehkan."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -209,12 +209,12 @@ export default function FormImage() {
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["image/jpeg", "image/png", "image/jpg"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
file.size <= 100 * 1024 * 1024
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .jpg, .jpeg, .png, maksimal 100MB yang diperbolehkan.",
|
||||
},
|
||||
}
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -261,8 +261,10 @@ export default function FormImage() {
|
|||
pointOfView: "None",
|
||||
clientId: "",
|
||||
};
|
||||
console.log("Sending request for title with data:", titleData);
|
||||
const titleRes = await getGenerateTitle(titleData);
|
||||
setTitle(titleRes?.data?.data || "");
|
||||
console.log("Generated title:", titleRes?.data?.data);
|
||||
|
||||
const keywordsData = {
|
||||
keyword: selectedMainKeyword,
|
||||
|
|
@ -273,9 +275,12 @@ export default function FormImage() {
|
|||
pointOfView: "None",
|
||||
clientId: "",
|
||||
};
|
||||
console.log("Sending request for keywords with data:", keywordsData);
|
||||
const keywordsRes = await getGenerateKeywords(keywordsData);
|
||||
setSelectedSEO(keywordsRes?.data?.data || []);
|
||||
console.log("Generated keywords:", keywordsRes?.data?.data);
|
||||
} catch (error) {
|
||||
console.error("Error during generation process:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -305,7 +310,9 @@ export default function FormImage() {
|
|||
console.log("Sending request for title with data:", titleData);
|
||||
const titleRes = await getGenerateTitle(titleData);
|
||||
setTitle(titleRes?.data?.data || "");
|
||||
console.log("Generated title:", titleRes?.data?.data);
|
||||
} catch (error) {
|
||||
console.error("Error generating title:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -315,6 +322,7 @@ export default function FormImage() {
|
|||
title: "WARNING",
|
||||
text: "Please provide a valid title.",
|
||||
});
|
||||
console.error("Please provide a valid main keyword.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -331,8 +339,10 @@ export default function FormImage() {
|
|||
pointOfView: "None",
|
||||
clientId: "",
|
||||
};
|
||||
console.log("Sending request for keywords with data:", keywordsData);
|
||||
const keywordsRes = await getGenerateKeywords(keywordsData);
|
||||
setSelectedSEO(keywordsRes?.data?.data || []);
|
||||
console.log("Generated keywords:", keywordsRes?.data?.data);
|
||||
} catch (error) {
|
||||
console.error("Error generating keywords:", error);
|
||||
} finally {
|
||||
|
|
@ -375,12 +385,7 @@ export default function FormImage() {
|
|||
return false;
|
||||
}
|
||||
|
||||
// const newArticleId = res?.data?.data?.id;
|
||||
const newArticleId =
|
||||
res?.data?.data?.id ||
|
||||
res?.data?.data?.articleId ||
|
||||
res?.data?.data?.uuid;
|
||||
|
||||
const newArticleId = res?.data?.data?.id;
|
||||
setIsGeneratedArticle(true);
|
||||
|
||||
setArticleIds((prevIds: string[]) => {
|
||||
|
|
@ -420,7 +425,7 @@ export default function FormImage() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
"",
|
||||
""
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -493,7 +498,7 @@ export default function FormImage() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -524,7 +529,7 @@ export default function FormImage() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id),
|
||||
.map((opt: any) => opt.id)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -546,7 +551,7 @@ export default function FormImage() {
|
|||
}
|
||||
}, [articleBody, setValue]);
|
||||
|
||||
const userId = Cookies.get("userId");
|
||||
const userId = Cookies.get("userId"); // atau dari auth context / localStorage
|
||||
|
||||
const save = async (data: ImageSchema) => {
|
||||
loading();
|
||||
|
|
@ -561,8 +566,8 @@ export default function FormImage() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
|
|
@ -588,10 +593,10 @@ export default function FormImage() {
|
|||
|
||||
// ✅ Sesuaikan dengan struktur Swagger
|
||||
const articleData: CreateArticleData = {
|
||||
aiArticleId: 0,
|
||||
aiArticleId: 0, // default 0
|
||||
categoryIds: selectedCategory.toString(),
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
createdById: Number(userId), // isi dengan userId valid
|
||||
description: htmlToString(finalDescription),
|
||||
htmlDescription: finalDescription,
|
||||
isDraft: true,
|
||||
|
|
@ -603,31 +608,9 @@ export default function FormImage() {
|
|||
.replace(/[^a-z0-9-]/g, ""),
|
||||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 1,
|
||||
|
||||
// 🔥 TAMBAHKAN INI
|
||||
publishedFor: data.publishedFor.join(","),
|
||||
typeId: 1, // Image content type
|
||||
};
|
||||
|
||||
// const articleData: CreateArticleData = {
|
||||
// aiArticleId: 0, // default 0
|
||||
// categoryIds: selectedCategory.toString(),
|
||||
// createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
|
||||
// createdById: Number(userId), // isi dengan userId valid
|
||||
// description: htmlToString(finalDescription),
|
||||
// htmlDescription: finalDescription,
|
||||
// isDraft: true,
|
||||
// isPublish: false,
|
||||
// oldId: 0,
|
||||
// slug: finalTitle
|
||||
// .toLowerCase()
|
||||
// .replace(/\s+/g, "-")
|
||||
// .replace(/[^a-z0-9-]/g, ""),
|
||||
// tags: finalTags,
|
||||
// title: finalTitle,
|
||||
// typeId: 1, // Image content type
|
||||
// };
|
||||
|
||||
let id = Cookies.get("idCreate");
|
||||
|
||||
if (id == undefined) {
|
||||
|
|
@ -639,7 +622,7 @@ export default function FormImage() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -658,7 +641,7 @@ export default function FormImage() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -674,7 +657,7 @@ export default function FormImage() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -716,7 +699,7 @@ export default function FormImage() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -752,7 +735,7 @@ export default function FormImage() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1007,43 +990,14 @@ export default function FormImage() {
|
|||
<div className="flex flex-row items-center gap-3 py-3 ">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* <Switch
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
id="c2"
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setIsSwitchOn(checked)
|
||||
}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
|
|
@ -1302,13 +1256,14 @@ export default function FormImage() {
|
|||
|
||||
<p className="text-sm font-semibold">Content Rewrite</p>
|
||||
<div className="my-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={handleRewriteClick}
|
||||
className="bg-blue-500 text-white py-2 px-3 rounded hover:bg-black"
|
||||
className="bg-blue-500 text-white py-2 px-4 rounded"
|
||||
>
|
||||
Content Rewrite
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showRewriteEditor && (
|
||||
|
|
@ -1538,19 +1493,19 @@ export default function FormImage() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
const handleChange = () => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = checked
|
||||
? options
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
.map((opt: any) => opt.id);
|
||||
} else {
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1561,48 +1516,17 @@ export default function FormImage() {
|
|||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
{/* <Checkbox
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/> */}
|
||||
/>
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import { useTranslations } from "next-intl";
|
|||
|
||||
const CustomEditor = dynamic(
|
||||
() => import("@/components/editor/custom-editor"),
|
||||
{ ssr: false },
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const imageSchema = z.object({
|
||||
|
|
@ -99,7 +99,7 @@ export default function FormImageUpdate() {
|
|||
setFiles((prev) => [
|
||||
...prev,
|
||||
...acceptedFiles.map((f) =>
|
||||
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }),
|
||||
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) })
|
||||
),
|
||||
]),
|
||||
accept: { "image/*": [] },
|
||||
|
|
@ -171,13 +171,12 @@ export default function FormImageUpdate() {
|
|||
const allOptions = options
|
||||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
);
|
||||
} else {
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -234,23 +233,6 @@ export default function FormImageUpdate() {
|
|||
// router.push("/admin/content/image");
|
||||
// });
|
||||
// };
|
||||
const formatDateTime = (date: Date) => {
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
|
||||
return (
|
||||
date.getFullYear() +
|
||||
"-" +
|
||||
pad(date.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(date.getDate()) +
|
||||
" " +
|
||||
pad(date.getHours()) +
|
||||
":" +
|
||||
pad(date.getMinutes()) +
|
||||
":" +
|
||||
pad(date.getSeconds())
|
||||
);
|
||||
};
|
||||
|
||||
// 🔹 ganti fungsi save di FormImageUpdate.tsx
|
||||
const save = async (data: ImageSchema) => {
|
||||
|
|
@ -266,10 +248,7 @@ export default function FormImageUpdate() {
|
|||
const payload = {
|
||||
aiArticleId: detail?.aiArticleId ?? null,
|
||||
categoryIds: selectedTarget ? String(selectedTarget) : "",
|
||||
// createdAt: detail?.createdAt ?? new Date().toISOString(),
|
||||
createdAt: detail?.createdAt
|
||||
? detail.createdAt.replace("T", " ").split("+")[0]
|
||||
: formatDateTime(new Date()),
|
||||
createdAt: detail?.createdAt ?? new Date().toISOString(),
|
||||
createdById: detail?.createdById ?? null,
|
||||
description: htmlToString(descFinal),
|
||||
htmlDescription: descFinal,
|
||||
|
|
@ -282,9 +261,9 @@ export default function FormImageUpdate() {
|
|||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, ""),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(", "),
|
||||
tags: tags,
|
||||
title: data.title,
|
||||
typeId: 1,
|
||||
typeId: 1, // 1 = image (sesuai struktur kamu)
|
||||
};
|
||||
|
||||
console.log("📤 Payload Update Article:", payload);
|
||||
|
|
@ -400,14 +379,14 @@ export default function FormImageUpdate() {
|
|||
!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}
|
||||
|
|
@ -468,7 +447,7 @@ export default function FormImageUpdate() {
|
|||
className="flex justify-between border p-3 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
{/* <Image
|
||||
<Image
|
||||
src={
|
||||
file.thumbnailFileUrl ||
|
||||
file.preview ||
|
||||
|
|
@ -478,25 +457,11 @@ export default function FormImageUpdate() {
|
|||
width={64}
|
||||
height={64}
|
||||
className="rounded border"
|
||||
/> */}
|
||||
<Image
|
||||
src={
|
||||
file.preview || // file baru (dropzone)
|
||||
file.thumbnailUrl || // dari backend jika ada
|
||||
file.fileUrl || // fallback utama dari backend
|
||||
"/placeholder.png"
|
||||
}
|
||||
alt={file.fileName || file.name || "file"}
|
||||
width={64}
|
||||
height={64}
|
||||
className="rounded border object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-medium">{file.fileName}</p>
|
||||
<a
|
||||
href={file.fileUrl || file.url}
|
||||
href={file.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 text-sm"
|
||||
|
|
@ -589,36 +554,6 @@ export default function FormImageUpdate() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{options.map((opt) => {
|
||||
const isAllSelected =
|
||||
publishedFor.length ===
|
||||
options.filter((o) => o.id !== "all").length;
|
||||
|
||||
const isChecked =
|
||||
opt.id === "all"
|
||||
? isAllSelected
|
||||
: publishedFor.includes(opt.id);
|
||||
|
||||
return (
|
||||
<div key={opt.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={opt.id}
|
||||
value={opt.id}
|
||||
checked={isChecked}
|
||||
onChange={() => handleCheckboxChange(opt.id)}
|
||||
className="w-4 h-4 accent-black"
|
||||
/>
|
||||
<Label htmlFor={opt.id}>{opt.name}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<Label>Publish Target</Label>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{options.map((opt) => (
|
||||
|
|
@ -637,19 +572,12 @@ export default function FormImageUpdate() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button type="submit">Simpan</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
className="hover:bg-gray-300"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
<Button
|
||||
className="hover:bg-gray-300"
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ 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("");
|
||||
|
|
@ -69,7 +68,6 @@ export default function SignUp() {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [formErrors, setFormErrors] = useState<any>({});
|
||||
const [tenantList, setTenantList] = useState<Tenant[]>([]);
|
||||
const [usernameKontributor, setUsernameKontributor] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getTenant();
|
||||
|
|
@ -96,7 +94,7 @@ export default function SignUp() {
|
|||
|
||||
// Kontributor (sementara ikut umum)
|
||||
if (role === "kontributor") {
|
||||
await handleCreateUserKontributor(e);
|
||||
await handleCreateUserUmum(e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -104,17 +102,6 @@ export default function SignUp() {
|
|||
setStep("otp");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (role === "kontributor" && firstNameKontributor && lastNameKontributor) {
|
||||
const generatedUsername = `${firstNameKontributor}-${lastNameKontributor}`
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "");
|
||||
|
||||
setUsernameKontributor((prev) => prev || generatedUsername);
|
||||
}
|
||||
}, [role, firstNameKontributor, lastNameKontributor]);
|
||||
|
||||
const handleVerifyOtp = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const code = otp.join("");
|
||||
|
|
@ -132,11 +119,7 @@ export default function SignUp() {
|
|||
if (value && nextInput) nextInput.focus();
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
const nameRegex = /^[A-Za-z\s]+$/;
|
||||
return nameRegex.test(value.trim());
|
||||
};
|
||||
|
||||
// Form validation functions
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
|
|
@ -151,65 +134,6 @@ 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();
|
||||
|
||||
if (!firstNameKontributor.trim() || !lastNameKontributor.trim()) {
|
||||
MySwal.fire(
|
||||
"Peringatan",
|
||||
"Nama depan dan belakang wajib diisi",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
MySwal.fire("Peringatan", "Email tidak valid", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePassword(kontributorPassword)) {
|
||||
MySwal.fire("Peringatan", "Password minimal 8 karakter", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const fullName = `${firstNameKontributor} ${lastNameKontributor}`;
|
||||
|
||||
const payload = {
|
||||
address: "",
|
||||
clientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
|
||||
email,
|
||||
fullName,
|
||||
password: kontributorPassword,
|
||||
phoneNumber: whatsappKontributor,
|
||||
// username: fullName.toLowerCase().replace(/\s+/g, "-"),
|
||||
username: usernameKontributor,
|
||||
userLevelId: 1,
|
||||
userRoleId: 5, // MISAL role kontributor
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await createUser(payload);
|
||||
|
||||
if (res?.error) {
|
||||
MySwal.fire("Gagal", res?.message || "Gagal mendaftar", "error");
|
||||
} else {
|
||||
MySwal.fire("Berhasil", "Akun kontributor berhasil dibuat", "success");
|
||||
router.push("/auth");
|
||||
}
|
||||
} catch (err) {
|
||||
MySwal.fire("Error", "Terjadi kesalahan server", "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateUserUmum = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -283,29 +207,12 @@ 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 wajib diisi";
|
||||
} else if (!validateName(firstName)) {
|
||||
errors.firstName = "First name hanya boleh huruf dan spasi";
|
||||
errors.firstName = "First name is required";
|
||||
}
|
||||
|
||||
// if (!lastName.trim()) {
|
||||
// errors.lastName = "Last name is required";
|
||||
// }
|
||||
if (!lastName.trim()) {
|
||||
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";
|
||||
errors.lastName = "Last name is required";
|
||||
}
|
||||
|
||||
if (!email.trim()) {
|
||||
|
|
@ -343,7 +250,9 @@ export default function SignUp() {
|
|||
const handleTenantRegistration = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateTenantForm()) return;
|
||||
if (!validateTenantForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setFormErrors({});
|
||||
|
|
@ -352,75 +261,43 @@ export default function SignUp() {
|
|||
const registrationData = {
|
||||
adminUser: {
|
||||
address: "Jakarta",
|
||||
email: email.trim(),
|
||||
fullname: `${firstName.trim()} ${lastName.trim()}`,
|
||||
email: email,
|
||||
fullname: `${firstName} ${lastName}`,
|
||||
password: tenantPassword,
|
||||
phoneNumber: whatsapp.trim(),
|
||||
username: tenantUsername.trim(),
|
||||
phoneNumber: whatsapp,
|
||||
username: `${firstName}-${lastName}`,
|
||||
},
|
||||
client: {
|
||||
clientType: "sub_client",
|
||||
name: namaTenant.trim(),
|
||||
name: namaTenant,
|
||||
parentClientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await registerTenant(registrationData);
|
||||
|
||||
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
|
||||
if (response.error) {
|
||||
MySwal.fire({
|
||||
title: "Registrasi Gagal",
|
||||
text: backendMessage || "Tenant gagal dibuat",
|
||||
title: "Registration Failed",
|
||||
text: response.message || "An error occurred during registration",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
MySwal.fire({
|
||||
title: "Registration Successful",
|
||||
text: "Your tenant account has been created successfully!",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
}).then(() => {
|
||||
router.push("/auth");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 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);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
MySwal.fire({
|
||||
title: "Terjadi Kesalahan",
|
||||
text: "Terjadi kesalahan server. Silakan coba lagi.",
|
||||
title: "Registration Failed",
|
||||
text: "An unexpected error occurred. Please try again.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
|
@ -429,66 +306,6 @@ 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()) {
|
||||
|
|
@ -708,18 +525,6 @@ export default function SignUp() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Username"
|
||||
value={usernameKontributor}
|
||||
onChange={(e) => setUsernameKontributor(e.target.value)}
|
||||
/>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
Username dibuat otomatis dari nama, tetapi dapat diubah.
|
||||
</p>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
|
|
@ -736,13 +541,13 @@ export default function SignUp() {
|
|||
onChange={(e) => setWhatsappKontributor(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* <Input
|
||||
<Input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Nama Perusahaan"
|
||||
value={namaPerusahaan}
|
||||
onChange={(e) => setNamaPerusahaan(e.target.value)}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<select
|
||||
required
|
||||
|
|
@ -827,9 +632,6 @@ 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}
|
||||
|
|
@ -855,32 +657,6 @@ 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"
|
||||
|
|
@ -1078,7 +854,7 @@ export default function SignUp() {
|
|||
{/* Link Login */}
|
||||
<p className="text-center text-sm mt-4">
|
||||
Sudah punya akun?{" "}
|
||||
<a href="/auth" className="text-[#007AFF] hover:underline">
|
||||
<a href="/login" className="text-[#007AFF] hover:underline">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -8,13 +8,7 @@ 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";
|
||||
|
|
@ -24,26 +18,19 @@ import {
|
|||
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" }),
|
||||
});
|
||||
|
|
@ -51,20 +38,11 @@ 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" }),
|
||||
});
|
||||
|
|
@ -89,11 +67,8 @@ 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,
|
||||
|
|
@ -101,9 +76,7 @@ export default function UserForm({
|
|||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<UserSchema>({
|
||||
resolver: zodResolver(
|
||||
mode === "create" ? createUserSchema : editUserSchema,
|
||||
),
|
||||
resolver: zodResolver(mode === "create" ? createUserSchema : editUserSchema),
|
||||
defaultValues: {
|
||||
address: "",
|
||||
clientId: "",
|
||||
|
|
@ -133,8 +106,7 @@ 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 {
|
||||
|
|
@ -160,10 +132,7 @@ 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);
|
||||
|
|
@ -192,7 +161,7 @@ export default function UserForm({
|
|||
password: data.password || "",
|
||||
phoneNumber: data.phoneNumber,
|
||||
userLevelId: data.userLevelId,
|
||||
userRoleId: 3,
|
||||
userRoleId: 3, // Hardcoded as per requirement
|
||||
username: data.username,
|
||||
};
|
||||
|
||||
|
|
@ -208,25 +177,18 @@ 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);
|
||||
}
|
||||
};
|
||||
|
|
@ -299,41 +261,6 @@ 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
|
||||
|
|
@ -347,7 +274,7 @@ export default function UserForm({
|
|||
<p className="text-red-500 text-sm">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
|
||||
{/* Phone Number */}
|
||||
<div>
|
||||
|
|
@ -360,9 +287,7 @@ 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>
|
||||
|
||||
|
|
@ -387,17 +312,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}
|
||||
|
|
@ -417,7 +342,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>
|
||||
|
|
@ -426,10 +351,7 @@ 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>
|
||||
|
|
@ -450,16 +372,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>
|
||||
|
|
|
|||
|
|
@ -2938,63 +2938,6 @@ export const UsersIcon = ({ size = 24, width, height, ...props }: IconSvgProps)
|
|||
</svg>
|
||||
);
|
||||
|
||||
export const MenuIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ModuleIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EditIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const WorkflowIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -3053,3 +2996,34 @@ export const RotateCcwIcon = ({ size = 24, width, height, ...props }: IconSvgPro
|
|||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EditIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ArticleCategory,
|
||||
} from "@/service/categories/article-categories";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { RevealR } from "../ui/RevealR";
|
||||
|
||||
export default function Category() {
|
||||
const t = useTranslations("MediaUpdate");
|
||||
|
|
@ -22,7 +21,7 @@ export default function Category() {
|
|||
// Filter hanya kategori yang aktif dan published
|
||||
const activeCategories = response.data.data.filter(
|
||||
(category: ArticleCategory) =>
|
||||
category.isActive && category.isPublish,
|
||||
category.isActive && category.isPublish
|
||||
);
|
||||
setCategories(activeCategories);
|
||||
}
|
||||
|
|
@ -56,58 +55,56 @@ export default function Category() {
|
|||
categories.length > 0 ? categories : fallbackCategories;
|
||||
|
||||
return (
|
||||
<RevealR>
|
||||
<section className="px-4 py-10">
|
||||
<div className="max-w-[1350px] mx-auto bg-white dark:bg-default-50 dark:border dark:border-slate-50 rounded-xl shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-5">
|
||||
{loading
|
||||
? t("loadCategory")
|
||||
: `${displayCategories.length} ${t("category")}`}
|
||||
</h2>
|
||||
<section className="px-4 py-10">
|
||||
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-5">
|
||||
{loading
|
||||
? t("loadCategory")
|
||||
: `${displayCategories.length} ${t("category")}`}
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
// Loading skeleton
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div
|
||||
{loading ? (
|
||||
// Loading skeleton
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-4 py-2 rounded border border-gray-200 bg-gray-100 animate-pulse"
|
||||
>
|
||||
<div className="h-4 w-20 bg-gray-300 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{displayCategories.map((category, index) => {
|
||||
// Handle both API data and fallback data
|
||||
const categoryTitle =
|
||||
typeof category === "string" ? category : category.title;
|
||||
const categorySlug =
|
||||
typeof category === "string"
|
||||
? category.toLowerCase().replace(/\s+/g, "-")
|
||||
: category.slug;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className="px-4 py-2 rounded border border-gray-200 bg-gray-100 animate-pulse"
|
||||
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
|
||||
onClick={() => {
|
||||
// Navigate to category page or search by category
|
||||
console.log(
|
||||
`Category clicked: ${categoryTitle} (${categorySlug})`
|
||||
);
|
||||
// TODO: Implement navigation to category page
|
||||
}}
|
||||
>
|
||||
<div className="h-4 w-20 bg-gray-300 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{displayCategories.map((category, index) => {
|
||||
// Handle both API data and fallback data
|
||||
const categoryTitle =
|
||||
typeof category === "string" ? category : category.title;
|
||||
const categorySlug =
|
||||
typeof category === "string"
|
||||
? category.toLowerCase().replace(/\s+/g, "-")
|
||||
: category.slug;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className="px-4 py-2 rounded border border-gray-300 text-gray-700 dark:text-white text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
|
||||
onClick={() => {
|
||||
// Navigate to category page or search by category
|
||||
console.log(
|
||||
`Category clicked: ${categoryTitle} (${categorySlug})`,
|
||||
);
|
||||
// TODO: Implement navigation to category page
|
||||
}}
|
||||
>
|
||||
{categoryTitle}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</RevealR>
|
||||
{categoryTitle}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import "swiper/css";
|
|||
import "swiper/css/navigation";
|
||||
import LocalSwitcher from "../partials/header/locale-switcher";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
// Custom styles for Swiper
|
||||
const swiperStyles = `
|
||||
|
|
@ -101,180 +100,178 @@ export default function Footer() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Reveal>
|
||||
<footer className="border-t bg-white dark:bg-default-50 text-center">
|
||||
<style jsx>{swiperStyles}</style>
|
||||
<div className="max-w-[1350px] mx-auto">
|
||||
<div className="py-6">
|
||||
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
||||
{t("publication")}
|
||||
</h2>
|
||||
<div className="px-4 md:px-12">
|
||||
<Swiper
|
||||
modules={[Navigation, Autoplay]}
|
||||
spaceBetween={24}
|
||||
slidesPerView="auto"
|
||||
centeredSlides={clients.length <= 4}
|
||||
navigation={{
|
||||
nextEl: ".swiper-button-next",
|
||||
prevEl: ".swiper-button-prev",
|
||||
}}
|
||||
autoplay={{
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
loop={clients.length > 4}
|
||||
className={`client-swiper ${
|
||||
clients.length <= 4 ? "swiper-centered" : ""
|
||||
}`}
|
||||
>
|
||||
{loading
|
||||
? // Loading skeleton
|
||||
Array.from({ length: 8 }).map((_, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
|
||||
</SwiperSlide>
|
||||
))
|
||||
: clients.length > 0
|
||||
? // Dynamic clients from API
|
||||
clients.map((client, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${client.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
>
|
||||
{client.logoUrl ? (
|
||||
<Image
|
||||
src={client.logoUrl}
|
||||
alt={client.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
) : (
|
||||
// Fallback when no logo - menggunakan placeholder image
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
|
||||
<Image
|
||||
src="/logo-netidhub.png"
|
||||
alt={`${client.name} placeholder`}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))
|
||||
: // Fallback to static logos if API fails or no data
|
||||
logos.map((logo, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${logo.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<footer className="border-t bg-white text-center">
|
||||
<style jsx>{swiperStyles}</style>
|
||||
<div className="max-w-[1350px] mx-auto">
|
||||
<div className="py-6">
|
||||
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
||||
{t("publication")}
|
||||
</h2>
|
||||
<div className="px-4 md:px-12">
|
||||
<Swiper
|
||||
modules={[Navigation, Autoplay]}
|
||||
spaceBetween={24}
|
||||
slidesPerView="auto"
|
||||
centeredSlides={clients.length <= 4}
|
||||
navigation={{
|
||||
nextEl: ".swiper-button-next",
|
||||
prevEl: ".swiper-button-prev",
|
||||
}}
|
||||
autoplay={{
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
loop={clients.length > 4}
|
||||
className={`client-swiper ${
|
||||
clients.length <= 4 ? "swiper-centered" : ""
|
||||
}`}
|
||||
>
|
||||
{loading
|
||||
? // Loading skeleton
|
||||
Array.from({ length: 8 }).map((_, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
|
||||
</SwiperSlide>
|
||||
))
|
||||
: clients.length > 0
|
||||
? // Dynamic clients from API
|
||||
clients.map((client, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${client.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
>
|
||||
{client.logoUrl ? (
|
||||
<Image
|
||||
src={client.logoUrl}
|
||||
alt={client.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
) : (
|
||||
// Fallback when no logo - menggunakan placeholder image
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`logo-${idx}`}
|
||||
width={80}
|
||||
height={80}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
src="/logo-netidhub.png"
|
||||
alt={`${client.name} placeholder`}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
|
||||
/>
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))
|
||||
: // Fallback to static logos if API fails or no data
|
||||
logos.map((logo, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${logo.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`logo-${idx}`}
|
||||
width={80}
|
||||
height={80}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
{/* <div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div> */}
|
||||
{/* <div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t my-6 w-full max-w-6xl mx-auto" />
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 px-4 pb-6 max-w-6xl mx-auto text-sm text-gray-600 dark:text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>ver 1.0.0 @2025 - {t("netidhub")}</span>
|
||||
<Image
|
||||
src="/qudo.png"
|
||||
alt="qudoco"
|
||||
width={80}
|
||||
height={80}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Social Media */}
|
||||
<div className="flex gap-3 text-gray-800 dark:text-white">
|
||||
<Instagram className="hover:text-black" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4.594 4.984a1 1 0 0 1 .941.429C7.011 7.572 8.783 8.47 10.75 8.674c.096-.841.323-1.672.75-2.404c.626-1.074 1.644-1.864 3.098-2.156c2.01-.404 3.54.324 4.427 1.215l1.792-.335a1 1 0 0 1 1.053 1.478l-1.72 3.022c.157 4.361-1.055 7.405-3.639 9.502c-1.37 1.112-3.332 1.743-5.485 1.938c-2.17.196-4.623-.041-7.061-.753a1 1 0 0 1 .007-1.922c1.226-.349 2.16-.65 3.003-1.177c-1.199-.636-2.082-1.468-2.707-2.416c-.868-1.318-1.19-2.788-1.254-4.113S3.141 8 3.343 7.115c.115-.505.249-1.011.434-1.495a1 1 0 0 1 .818-.636Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M9.935 14.628v-5.62l5.403 2.82zM21.8 8.035s-.195-1.379-.795-1.986c-.76-.796-1.613-.8-2.004-.847C16.203 5 12.004 5 12.004 5h-.008s-4.198 0-6.997.202c-.391.047-1.243.05-2.004.847c-.6.607-.795 1.986-.795 1.986S2 9.653 2 11.272v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.76.797 1.76.771 2.205.855c1.6.153 6.8.2 6.8.2s4.203-.006 7.001-.208c.391-.047 1.244-.05 2.004-.847c.6-.607.795-1.985.795-1.985s.2-1.619.2-3.237v-1.517c0-1.619-.2-3.237-.2-3.237"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
// fill-rule="evenodd"
|
||||
>
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 2a2 2 0 0 1 2 2a3.004 3.004 0 0 0 2.398 2.94a2 2 0 0 1-.796 3.92A7 7 0 0 1 16 10.325V16a6 6 0 1 1-7.499-5.81a2 2 0 0 1 .998 3.873A2.002 2.002 0 0 0 10 18a2 2 0 0 0 2-2V4a2 2 0 0 1 2-2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* button language */}
|
||||
<div className={`relative text-left border rounded-lg`}>
|
||||
<LocalSwitcher />
|
||||
</div>
|
||||
{/* Navigation Buttons */}
|
||||
{/* <div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div> */}
|
||||
{/* <div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Reveal>
|
||||
|
||||
<div className="border-t my-6 w-full max-w-6xl mx-auto" />
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 px-4 pb-6 max-w-6xl mx-auto text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>ver 1.0.0 @2025 - {t("netidhub")}</span>
|
||||
<Image
|
||||
src="/qudo.png"
|
||||
alt="qudoco"
|
||||
width={80}
|
||||
height={80}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Social Media */}
|
||||
<div className="flex gap-3 text-gray-800">
|
||||
<Instagram className="hover:text-black" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4.594 4.984a1 1 0 0 1 .941.429C7.011 7.572 8.783 8.47 10.75 8.674c.096-.841.323-1.672.75-2.404c.626-1.074 1.644-1.864 3.098-2.156c2.01-.404 3.54.324 4.427 1.215l1.792-.335a1 1 0 0 1 1.053 1.478l-1.72 3.022c.157 4.361-1.055 7.405-3.639 9.502c-1.37 1.112-3.332 1.743-5.485 1.938c-2.17.196-4.623-.041-7.061-.753a1 1 0 0 1 .007-1.922c1.226-.349 2.16-.65 3.003-1.177c-1.199-.636-2.082-1.468-2.707-2.416c-.868-1.318-1.19-2.788-1.254-4.113S3.141 8 3.343 7.115c.115-.505.249-1.011.434-1.495a1 1 0 0 1 .818-.636Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M9.935 14.628v-5.62l5.403 2.82zM21.8 8.035s-.195-1.379-.795-1.986c-.76-.796-1.613-.8-2.004-.847C16.203 5 12.004 5 12.004 5h-.008s-4.198 0-6.997.202c-.391.047-1.243.05-2.004.847c-.6.607-.795 1.986-.795 1.986S2 9.653 2 11.272v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.76.797 1.76.771 2.205.855c1.6.153 6.8.2 6.8.2s4.203-.006 7.001-.208c.391-.047 1.244-.05 2.004-.847c.6-.607.795-1.985.795-1.985s.2-1.619.2-3.237v-1.517c0-1.619-.2-3.237-.2-3.237"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
// fill-rule="evenodd"
|
||||
>
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 2a2 2 0 0 1 2 2a3.004 3.004 0 0 0 2.398 2.94a2 2 0 0 1-.796 3.92A7 7 0 0 1 16 10.325V16a6 6 0 1 1-7.499-5.81a2 2 0 0 1 .998 3.873A2.002 2.002 0 0 0 10 18a2 2 0 0 0 2-2V4a2 2 0 0 1 2-2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* button language */}
|
||||
<div className={`relative text-left border rounded-lg`}>
|
||||
<LocalSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ import "swiper/css/navigation";
|
|||
import "swiper/css/pagination";
|
||||
import ImageBlurry from "../ui/image-blurry";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Reveal } from "../ui/Reveal";
|
||||
import { RevealL } from "../ui/RevealL";
|
||||
import { RevealR } from "../ui/RevealR";
|
||||
|
||||
const images = ["/PPS.png", "/PPS2.jpeg", "/PPS3.jpg", "/PPS4.png"];
|
||||
|
||||
|
|
@ -44,7 +41,7 @@ export default function Header() {
|
|||
undefined,
|
||||
undefined,
|
||||
"createdAt",
|
||||
slug,
|
||||
slug
|
||||
);
|
||||
|
||||
let articlesData: any[] = [];
|
||||
|
|
@ -59,14 +56,14 @@ export default function Header() {
|
|||
"createdAt",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
);
|
||||
articlesData = (fallbackResponse?.data?.data?.content || []).filter(
|
||||
(item: any) => item.typeId === 1,
|
||||
(item: any) => item.typeId === 1
|
||||
);
|
||||
} else {
|
||||
articlesData = (response?.data?.data || []).filter(
|
||||
(item: any) => item.typeId === 1,
|
||||
(item: any) => item.typeId === 1
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -109,14 +106,14 @@ export default function Header() {
|
|||
const ids = new Set<number>(
|
||||
(Array.isArray(bookmarks) ? bookmarks : [])
|
||||
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
||||
.filter((x) => !isNaN(x)),
|
||||
.filter((x) => !isNaN(x))
|
||||
);
|
||||
|
||||
const merged = new Set([...localSet, ...ids]);
|
||||
setBookmarkedIds(merged);
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(merged)),
|
||||
JSON.stringify(Array.from(merged))
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -131,77 +128,75 @@ export default function Header() {
|
|||
if (bookmarkedIds.size > 0) {
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(bookmarkedIds)),
|
||||
JSON.stringify(Array.from(bookmarkedIds))
|
||||
);
|
||||
}
|
||||
}, [bookmarkedIds]);
|
||||
|
||||
return (
|
||||
<RevealR>
|
||||
<section className="max-w-[1350px] mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||
{data.length > 0 && (
|
||||
<section className="max-w-[1350px] mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||
{data.length > 0 && (
|
||||
<Card
|
||||
item={data[0]}
|
||||
isBig
|
||||
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
|
||||
onSaved={(id) =>
|
||||
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
|
||||
{data.slice(1, 5).map((item) => (
|
||||
<Card
|
||||
item={data[0]}
|
||||
isBig
|
||||
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
|
||||
key={item.id}
|
||||
item={item}
|
||||
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
|
||||
onSaved={(id) =>
|
||||
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
|
||||
{data.slice(1, 5).map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
item={item}
|
||||
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
|
||||
onSaved={(id) =>
|
||||
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
|
||||
<Swiper
|
||||
modules={[Navigation, Pagination]}
|
||||
navigation
|
||||
pagination={{ clickable: true }}
|
||||
spaceBetween={10}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
className="w-full h-full"
|
||||
>
|
||||
{images.map((img, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px]">
|
||||
{/* <Image
|
||||
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
|
||||
<Swiper
|
||||
modules={[Navigation, Pagination]}
|
||||
navigation
|
||||
pagination={{ clickable: true }}
|
||||
spaceBetween={10}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
className="w-full h-full"
|
||||
>
|
||||
{images.map((img, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px]">
|
||||
{/* <Image
|
||||
src={img}
|
||||
alt={`slide-${index}`}
|
||||
fill
|
||||
className="object-cover rounded-xl"
|
||||
priority={index === 0}
|
||||
/> */}
|
||||
<ImageBlurry
|
||||
priority
|
||||
src={img}
|
||||
alt="gambar"
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</section>
|
||||
</RevealR>
|
||||
<ImageBlurry
|
||||
priority
|
||||
src={img}
|
||||
alt="gambar"
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -221,29 +216,6 @@ function Card({
|
|||
const MySwal = withReactContent(Swal);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
|
||||
const DEFAULT_IMAGE = "/assets/logo1.png";
|
||||
|
||||
const [imageSrc, setImageSrc] = useState(DEFAULT_IMAGE);
|
||||
|
||||
useEffect(() => {
|
||||
const src = item?.smallThumbnailLink;
|
||||
|
||||
if (!src) {
|
||||
setImageSrc(DEFAULT_IMAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new window.Image();
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
setImageSrc(src);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
setImageSrc(DEFAULT_IMAGE);
|
||||
};
|
||||
}, [item?.smallThumbnailLink]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsBookmarked(isInitiallyBookmarked);
|
||||
|
|
@ -285,7 +257,7 @@ function Card({
|
|||
newSet.add(Number(item.id));
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(newSet)),
|
||||
JSON.stringify(Array.from(newSet))
|
||||
);
|
||||
|
||||
MySwal.fire({
|
||||
|
|
@ -311,9 +283,9 @@ function Card({
|
|||
};
|
||||
|
||||
return (
|
||||
<RevealL>
|
||||
<div>
|
||||
<div
|
||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white dark:bg-black dark:border dark:border-slate-50 ${
|
||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
|
||||
isBig
|
||||
? "w-full lg:max-w-[670px] lg:min-h-[680px]"
|
||||
: "w-full h-[350px] md:h-[330px]"
|
||||
|
|
@ -325,22 +297,17 @@ function Card({
|
|||
} w-full`}
|
||||
>
|
||||
<Link href={getLink()}>
|
||||
{/* <Image
|
||||
<Image
|
||||
src={item.smallThumbnailLink || "/contributor.png"}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/> */}
|
||||
<ImageBlurry
|
||||
src={imageSrc}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="py-[26px] px-4 space-y-2">
|
||||
<div className="flex justify-between items-center gap-2 text-xs font-semibold flex-row">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
|
||||
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
|
||||
{item.clientName}
|
||||
</span>
|
||||
|
|
@ -396,7 +363,7 @@ function Card({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RevealL>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import Swal from "sweetalert2";
|
|||
import withReactContent from "sweetalert2-react-content";
|
||||
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ImageBlurry from "../ui/image-blurry";
|
||||
import { RevealL } from "../ui/RevealL";
|
||||
|
||||
function formatTanggal(dateString: string) {
|
||||
if (!dateString) return "";
|
||||
|
|
@ -54,8 +52,6 @@ export default function MediaUpdate() {
|
|||
|
||||
const slug = params?.slug as string;
|
||||
|
||||
const DEFAULT_IMAGE = "/assets/logo1.png";
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(tab);
|
||||
}, [tab, slug]);
|
||||
|
|
@ -201,16 +197,6 @@ export default function MediaUpdate() {
|
|||
const typeId = parseInt(getTypeIdByContentType(contentType));
|
||||
setCurrentTypeId(typeId.toString());
|
||||
|
||||
// const response = await listArticles(
|
||||
// 1,
|
||||
// 10,
|
||||
// typeId,
|
||||
// undefined,
|
||||
// undefined,
|
||||
// section === "latest" ? "createdAt" : "viewCount",
|
||||
// slug
|
||||
// );
|
||||
|
||||
const response = await listArticles(
|
||||
1,
|
||||
10,
|
||||
|
|
@ -218,13 +204,13 @@ export default function MediaUpdate() {
|
|||
undefined,
|
||||
undefined,
|
||||
section === "latest" ? "createdAt" : "viewCount",
|
||||
slug || undefined, // ⬅️ jangan kirim undefined string
|
||||
slug
|
||||
);
|
||||
|
||||
let hasil: any[] = [];
|
||||
|
||||
if (response?.error) {
|
||||
// console.error("Articles API failed, fallback ke old API");
|
||||
console.error("Articles API failed, fallback ke old API");
|
||||
const fallbackRes = await listData(
|
||||
typeId.toString(),
|
||||
"",
|
||||
|
|
@ -233,7 +219,7 @@ export default function MediaUpdate() {
|
|||
0,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
);
|
||||
hasil = fallbackRes?.data?.data?.content || [];
|
||||
} else {
|
||||
|
|
@ -275,14 +261,14 @@ export default function MediaUpdate() {
|
|||
const ids = new Set<number>(
|
||||
(Array.isArray(bookmarks) ? bookmarks : [])
|
||||
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
||||
.filter((x) => !isNaN(x)),
|
||||
.filter((x) => !isNaN(x))
|
||||
);
|
||||
|
||||
const gabungan = new Set([...localSet, ...ids]);
|
||||
setBookmarkedIds(gabungan);
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(gabungan)),
|
||||
JSON.stringify(Array.from(gabungan))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -320,7 +306,7 @@ export default function MediaUpdate() {
|
|||
setBookmarkedIds(updated);
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(updated)),
|
||||
JSON.stringify(Array.from(updated))
|
||||
);
|
||||
|
||||
MySwal.fire({
|
||||
|
|
@ -341,75 +327,44 @@ export default function MediaUpdate() {
|
|||
}
|
||||
};
|
||||
|
||||
function SafeImage({ src, alt }: { src?: string; alt?: string }) {
|
||||
const [imgSrc, setImgSrc] = useState(DEFAULT_IMAGE);
|
||||
|
||||
useEffect(() => {
|
||||
if (!src) {
|
||||
setImgSrc(DEFAULT_IMAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new window.Image();
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
setImgSrc(src);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
setImgSrc(DEFAULT_IMAGE);
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<ImageBlurry
|
||||
src={imgSrc}
|
||||
alt={alt || "Image"}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RevealL>
|
||||
<section className="bg-white dark:bg-default-50 px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
|
||||
<div className="max-w-screen-xl mx-auto">
|
||||
<h2 className="text-2xl font-semibold text-center mb-6">
|
||||
{t("title")}
|
||||
</h2>
|
||||
<section className="bg-white px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
|
||||
<div className="max-w-screen-xl mx-auto">
|
||||
<h2 className="text-2xl font-semibold text-center mb-6">
|
||||
{t("title")}
|
||||
</h2>
|
||||
|
||||
{/* Main Tab */}
|
||||
<div className="flex justify-center mb-6 bg-white dark:bg-default-50">
|
||||
<Card className="bg-[#FFFFFF] dark:bg-default-50 rounded-xl flex flex-row p-3 gap-2 shadow-md border border-gray-200">
|
||||
<button
|
||||
onClick={() => setTab("latest")}
|
||||
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||
tab === "latest"
|
||||
? "bg-[#C6A455] text-white shadow-sm"
|
||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||
}`}
|
||||
>
|
||||
{t("latest")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("popular")}
|
||||
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||
tab === "popular"
|
||||
? "bg-[#C6A455] text-white shadow-sm"
|
||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||
}`}
|
||||
>
|
||||
{t("popular")}{" "}
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Main Tab */}
|
||||
<div className="flex justify-center mb-6 bg-white">
|
||||
<Card className="bg-[#FFFFFF] rounded-xl flex flex-row p-3 gap-2 shadow-md border border-gray-200">
|
||||
<button
|
||||
onClick={() => setTab("latest")}
|
||||
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||
tab === "latest"
|
||||
? "bg-[#C6A455] text-white shadow-sm"
|
||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||
}`}
|
||||
>
|
||||
{t("latest")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("popular")}
|
||||
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
|
||||
tab === "popular"
|
||||
? "bg-[#C6A455] text-white shadow-sm"
|
||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||
}`}
|
||||
>
|
||||
{t("popular")}{" "}
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Content Type Filter */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg dark:bg-default-50">
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{/* <button
|
||||
{/* Content Type Filter */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg">
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{/* <button
|
||||
onClick={() => setContentType("all")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "all"
|
||||
|
|
@ -420,181 +375,175 @@ export default function MediaUpdate() {
|
|||
📋 Semua
|
||||
</button> */}
|
||||
|
||||
<button
|
||||
onClick={() => setContentType("foto")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "foto"
|
||||
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
|
||||
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
📸 {t("image")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("audiovisual")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "audiovisual"
|
||||
? "bg-gradient-to-r from-purple-500 to-pink-600 text-white shadow-lg ring-2 ring-purple-300"
|
||||
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
🎬 {t("video")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("audio")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "audio"
|
||||
? "bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg ring-2 ring-green-300"
|
||||
: "bg-white text-green-600 border-2 border-green-200 hover:border-green-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
🎵 Audio
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("text")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "text"
|
||||
? "bg-gradient-to-r from-gray-500 to-slate-600 text-white shadow-lg ring-2 ring-gray-300"
|
||||
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
📝 {t("text")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setContentType("foto")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "foto"
|
||||
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
|
||||
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
📸 {t("image")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("audiovisual")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "audiovisual"
|
||||
? "bg-gradient-to-r from-purple-500 to-pink-600 text-white shadow-lg ring-2 ring-purple-300"
|
||||
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
🎬 {t("video")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("audio")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "audio"
|
||||
? "bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg ring-2 ring-green-300"
|
||||
: "bg-white text-green-600 border-2 border-green-200 hover:border-green-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
🎵 Audio
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContentType("text")}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||
contentType === "text"
|
||||
? "bg-gradient-to-r from-gray-500 to-slate-600 text-white shadow-lg ring-2 ring-gray-300"
|
||||
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
📝 {t("text")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
{loading ? (
|
||||
<p className="text-center">{t("loadContent")}</p>
|
||||
) : (
|
||||
<Swiper
|
||||
modules={[Navigation]}
|
||||
navigation
|
||||
spaceBetween={20}
|
||||
slidesPerView={1}
|
||||
breakpoints={{
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 4 },
|
||||
}}
|
||||
>
|
||||
{filteredData.map((item) => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<div className="rounded-xl shadow-md overflow-hidden bg-white dark:bg-default-50 dark:border dark:border-slate-50">
|
||||
{/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */}
|
||||
{item.typeId === 3 ? (
|
||||
// 📝 TEXT
|
||||
<div className="bg-[#e0c350] flex items-center justify-center h-[204px] text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : item.typeId === 4 ? (
|
||||
// 🎵 AUDIO
|
||||
<div className="flex items-center justify-center bg-[#bb3523] w-full h-[204px] text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
// 🎬 FOTO / VIDEO (default)
|
||||
<div className="w-full h-[204px] relative">
|
||||
<Link href={getLink(item)}>
|
||||
<SafeImage
|
||||
src={item.smallThumbnailLink}
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
{/* <Image
|
||||
{/* Slider */}
|
||||
{loading ? (
|
||||
<p className="text-center">{t("loadContent")}</p>
|
||||
) : (
|
||||
<Swiper
|
||||
modules={[Navigation]}
|
||||
navigation
|
||||
spaceBetween={20}
|
||||
slidesPerView={1}
|
||||
breakpoints={{
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 4 },
|
||||
}}
|
||||
>
|
||||
{filteredData.map((item) => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<div className="rounded-xl shadow-md overflow-hidden bg-white">
|
||||
{/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */}
|
||||
{item.typeId === 3 ? (
|
||||
// 📝 TEXT
|
||||
<div className="bg-[#e0c350] flex items-center justify-center h-[204px] text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="90"
|
||||
height="90"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : item.typeId === 4 ? (
|
||||
// 🎵 AUDIO
|
||||
<div className="flex items-center justify-center bg-[#bb3523] w-full h-[204px] text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
// 🎬 FOTO / VIDEO (default)
|
||||
<div className="w-full h-[204px] relative">
|
||||
<Link href={getLink(item)}>
|
||||
<Image
|
||||
src={item.smallThumbnailLink || "/placeholder.png"}
|
||||
alt={item.title || "No Title"}
|
||||
fill
|
||||
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
|
||||
/> */}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Caption / info */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold flex-row justify-between mb-2">
|
||||
<span className="text-xs text-white px-2 py-0.5 rounded bg-emerald-600">
|
||||
{item.clientName}
|
||||
</span>
|
||||
<span className="text-orange-600">
|
||||
{item.categories
|
||||
?.map((cat: any) => cat.title)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-1">
|
||||
{formatTanggal(item.createdAt)}
|
||||
</p>
|
||||
<Link href={getLink(item)}>
|
||||
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
|
||||
{item.title}
|
||||
</p>
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2 text-gray-600">
|
||||
<ThumbsUp className="w-4 h-4 cursor-pointer" />
|
||||
<ThumbsDown className="w-4 h-4 cursor-pointer" />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleSave(item.id)}
|
||||
disabled={bookmarkedIds.has(Number(item.id))}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className={`rounded px-4 ${
|
||||
bookmarkedIds.has(Number(item.id))
|
||||
? "bg-gray-400 cursor-not-allowed text-white"
|
||||
: "bg-red-700 text-white hover:bg-red-500"
|
||||
}`}
|
||||
>
|
||||
{bookmarkedIds.has(Number(item.id))
|
||||
? t("saved")
|
||||
: t("save")}
|
||||
</Button>
|
||||
{/* Caption / info */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap mb-2">
|
||||
<span className="text-xs text-white px-2 py-0.5 rounded bg-emerald-600">
|
||||
{item.clientName || "Tanpa Kategori"}
|
||||
</span>
|
||||
<span className="text-orange-600">
|
||||
{item.categories
|
||||
?.map((cat: any) => cat.title)
|
||||
.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-1">
|
||||
{formatTanggal(item.createdAt)}
|
||||
</p>
|
||||
<Link href={getLink(item)}>
|
||||
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
|
||||
{item.title}
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2 text-gray-600">
|
||||
<ThumbsUp className="w-4 h-4 cursor-pointer" />
|
||||
<ThumbsDown className="w-4 h-4 cursor-pointer" />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleSave(item.id)}
|
||||
disabled={bookmarkedIds.has(Number(item.id))}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className={`rounded px-4 ${
|
||||
bookmarkedIds.has(Number(item.id))
|
||||
? "bg-gray-400 cursor-not-allowed text-white"
|
||||
: "bg-red-700 text-white hover:bg-red-500"
|
||||
}`}
|
||||
>
|
||||
{bookmarkedIds.has(Number(item.id))
|
||||
? t("saved")
|
||||
: t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
)}
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
)}
|
||||
|
||||
{/* Lihat lebih banyak - hanya muncul jika ada data */}
|
||||
{filteredData.length > 0 && (
|
||||
<div className="text-center mt-10">
|
||||
<Link href={getContentTypeLink()}>
|
||||
<Button
|
||||
size={"lg"}
|
||||
className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition"
|
||||
>
|
||||
{t("seeMore")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</RevealL>
|
||||
{/* Lihat lebih banyak - hanya muncul jika ada data */}
|
||||
{filteredData.length > 0 && (
|
||||
<div className="text-center mt-10">
|
||||
<Link href={getContentTypeLink()}>
|
||||
<Button
|
||||
size={"lg"}
|
||||
className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition"
|
||||
>
|
||||
{t("seeMore")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import { Link } from "@/i18n/routing";
|
|||
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
|
||||
import { useTranslations } from "next-intl";
|
||||
import LocalSwitcher from "../partials/header/locale-switcher";
|
||||
import ThemeSwitcher from "../partials/header/theme-switcher";
|
||||
import { RevealT } from "../ui/RevealT";
|
||||
|
||||
export default function Navbar() {
|
||||
const t = useTranslations("Navbar");
|
||||
|
|
@ -76,218 +74,211 @@ export default function Navbar() {
|
|||
const fullname = Cookies.get("ufne");
|
||||
|
||||
return (
|
||||
<RevealT>
|
||||
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white dark:bg-default-50 z-50">
|
||||
<div className="flex flex-row items-center justify-between space-x-4 z-10">
|
||||
<Menu
|
||||
className="w-6 h-6 cursor-pointer"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
|
||||
<div className="flex flex-row items-center justify-between space-x-4 z-10">
|
||||
<Menu
|
||||
className="w-6 h-6 cursor-pointer"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
/>
|
||||
|
||||
<Link href="/" className="relative w-32 h-20">
|
||||
<Image
|
||||
src="/assets/logo1.png"
|
||||
alt="Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link href="/" className="relative w-32 h-20">
|
||||
<Image
|
||||
src="/assets/logo1.png"
|
||||
alt="Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</Link>
|
||||
<DynamicLogoTenant />
|
||||
</div>
|
||||
|
||||
<DynamicLogoTenant />
|
||||
{/* 🌐 NAV MENU */}
|
||||
<nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium">
|
||||
{filteredNavItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
<div className="hidden custom-lg-button:flex items-end">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🌐 NAV MENU */}
|
||||
<nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium">
|
||||
{filteredNavItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
// 🔹 Pengecekan khusus untuk "Untuk Anda"
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (item.label === t("forYou")) {
|
||||
e.preventDefault();
|
||||
if (!checkLoginStatus()) {
|
||||
router.push("/auth");
|
||||
} else {
|
||||
router.push("/in/for-you");
|
||||
}
|
||||
// 🔹 Pengecekan khusus untuk "Untuk Anda"
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (item.label === t("forYou")) {
|
||||
e.preventDefault();
|
||||
if (!checkLoginStatus()) {
|
||||
router.push("/auth");
|
||||
} else {
|
||||
router.push("/in/for-you");
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={item.label} className="relative">
|
||||
{item.label === t("publication") ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!isDropdownOpen)}
|
||||
className={cn(
|
||||
"relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors",
|
||||
isDropdownOpen ||
|
||||
pathname.startsWith("/public/publication")
|
||||
? "text-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded transition-all",
|
||||
isDropdownOpen ||
|
||||
pathname.startsWith("/public/publication")
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full mt-2 w-48 bg-white border rounded shadow z-50">
|
||||
{PUBLIKASI_SUBMENU.map((sub) => (
|
||||
<Link
|
||||
key={sub.label}
|
||||
href={sub.href}
|
||||
className="block px-4 py-2 text-sm hover:bg-gray-100 text-gray-700 dark:text-white"
|
||||
>
|
||||
{sub.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleClick}
|
||||
return (
|
||||
<div key={item.label} className="relative">
|
||||
{item.label === t("publication") ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!isDropdownOpen)}
|
||||
className={cn(
|
||||
"relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors",
|
||||
isActive && "text-black",
|
||||
"relative text-gray-500 hover:text-black transition-colors",
|
||||
isDropdownOpen ||
|
||||
pathname.startsWith("/public/publication")
|
||||
? "text-black"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{isActive && (
|
||||
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded" />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 🔹 PROFILE / LOGIN SECTION */}
|
||||
<nav className="hidden md:flex items-center gap-3 z-10 relative">
|
||||
{!isLoggedIn ? (
|
||||
<>
|
||||
<Link href="/auth/register">
|
||||
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white cursor-pointer">
|
||||
{t("register")}{" "}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth">
|
||||
<Button className="bg-red-700 text-white cursor-pointer hover:bg-white hover:border hover:border-red-700 hover:text-red-700">
|
||||
{t("login")}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowProfileMenu((prev) => !prev)}
|
||||
className="flex items-center gap-2 border-2 py-1 px-3 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full overflow-hidden border">
|
||||
<Image
|
||||
src="/avatar-profile.png"
|
||||
alt={username || "User avatar"}
|
||||
width={36}
|
||||
height={36}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-white">
|
||||
{fullname}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
{showProfileMenu && (
|
||||
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-black border rounded shadow z-50 ">
|
||||
<Link
|
||||
href="/admin/dashboard"
|
||||
className="flex flex-row items-center text-left px-4 py-2 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-700 dark:text-white"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 21a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM4 13a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm5-2V5H5v6zM4 21a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1zm1-2h4v-2H5zm10 0h4v-6h-4zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2 1v2h4V5z"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex flex-row text-left px-4 py-2 hover:bg-gray-300 text-gray-700 dark:text-white cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="M13.477 21.245H8.34a4.92 4.92 0 0 1-5.136-4.623V7.378A4.92 4.92 0 0 1 8.34 2.755h5.136"
|
||||
/>
|
||||
<path strokeMiterlimit="10" d="M20.795 12H7.442" />
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="m16.083 17.136l4.404-4.404a1.04 1.04 0 0 0 0-1.464l-4.404-4.404"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{t("logout")}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded transition-all",
|
||||
isDropdownOpen ||
|
||||
pathname.startsWith("/public/publication")
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full mt-2 w-48 bg-white border rounded shadow z-50">
|
||||
{PUBLIKASI_SUBMENU.map((sub) => (
|
||||
<Link
|
||||
key={sub.label}
|
||||
href={sub.href}
|
||||
className="block px-4 py-2 text-sm hover:bg-gray-100 text-gray-700"
|
||||
>
|
||||
{sub.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"relative text-gray-500 hover:text-black transition-colors",
|
||||
isActive && "text-black"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{isActive && (
|
||||
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded" />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* 📱 SIDEBAR MOBILE */}
|
||||
{isSidebarOpen && (
|
||||
<div className="fixed inset-0 z-50 flex">
|
||||
<div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="absolute top-4 right-4 text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{/* 🔹 PROFILE / LOGIN SECTION */}
|
||||
<nav className="hidden md:flex items-center gap-3 z-10 relative">
|
||||
{!isLoggedIn ? (
|
||||
<>
|
||||
<Link href="/auth/register">
|
||||
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white">
|
||||
{t("register")}{" "}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth">
|
||||
<Button className="bg-red-700 text-white">{t("login")}</Button>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowProfileMenu((prev) => !prev)}
|
||||
className="flex items-center gap-2 border-2 py-1 px-3 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full overflow-hidden border">
|
||||
<Image
|
||||
src="/avatar-profile.png"
|
||||
alt={username || "User avatar"}
|
||||
width={36}
|
||||
height={36}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{fullname}
|
||||
</span>
|
||||
<ChevronDown className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
<div className="mt-10">
|
||||
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
||||
{t("language")}
|
||||
</h3>
|
||||
{/* button language */}
|
||||
<div className={`relative text-left border w-fit rounded-lg`}>
|
||||
<LocalSwitcher />
|
||||
</div>
|
||||
{/* <div className="space-y-5 ml-3">
|
||||
{showProfileMenu && (
|
||||
<div className="absolute right-0 mt-2 w-40 bg-white border rounded shadow z-50 ">
|
||||
<Link
|
||||
href="/admin/dashboard"
|
||||
className="flex flex-row items-center text-left px-4 py-2 hover:bg-gray-100 text-gray-700"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 21a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM4 13a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm5-2V5H5v6zM4 21a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1zm1-2h4v-2H5zm10 0h4v-6h-4zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2 1v2h4V5z"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex flex-row text-left px-4 py-2 hover:bg-gray-100 text-gray-700 cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="M13.477 21.245H8.34a4.92 4.92 0 0 1-5.136-4.623V7.378A4.92 4.92 0 0 1 8.34 2.755h5.136"
|
||||
/>
|
||||
<path strokeMiterlimit="10" d="M20.795 12H7.442" />
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
d="m16.083 17.136l4.404-4.404a1.04 1.04 0 0 0 0-1.464l-4.404-4.404"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{t("logout")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* 📱 SIDEBAR MOBILE */}
|
||||
{isSidebarOpen && (
|
||||
<div className="fixed inset-0 z-50 flex">
|
||||
<div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
className="absolute top-4 right-4 text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div className="mt-10">
|
||||
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
||||
{t("language")}
|
||||
</h3>
|
||||
{/* button language */}
|
||||
<div className={`relative text-left border w-fit rounded-lg`}>
|
||||
<LocalSwitcher />
|
||||
</div>
|
||||
{/* <div className="space-y-5 ml-3">
|
||||
<button className="flex items-center gap-2 text-sm text-gray-800">
|
||||
<Image src={"/Flag.svg"} width={24} height={24} alt="flag" />
|
||||
English
|
||||
|
|
@ -311,99 +302,86 @@ export default function Navbar() {
|
|||
Bahasa Indonesia
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-[16px] font-bold text-gray-700 dark:text-white mb-2">
|
||||
{t("features")}
|
||||
</h3>
|
||||
<div className="space-y-5 ml-3">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={() => {
|
||||
if (item.label === t("forYou")) {
|
||||
if (!checkLoginStatus()) {
|
||||
router.push("/auth");
|
||||
} else {
|
||||
router.push("/for-you");
|
||||
}
|
||||
} else {
|
||||
router.push(item.href);
|
||||
}
|
||||
setIsSidebarOpen(false);
|
||||
}}
|
||||
className="block text-[15px] text-gray-800 dark:text-white text-left w-full"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 text-[16px] font-bold">
|
||||
<Link
|
||||
href="/about"
|
||||
className="block text-black dark:text-white"
|
||||
>
|
||||
{t("about")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/advertising"
|
||||
className="block text-black dark:text-white"
|
||||
>
|
||||
{t("advertising")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="block text-black dark:text-white"
|
||||
>
|
||||
{t("contact")}
|
||||
</Link>
|
||||
|
||||
{!isLoggedIn ? (
|
||||
<>
|
||||
<Link
|
||||
href="/auth"
|
||||
className="block text-lg text-gray-800 dark:text-white"
|
||||
>
|
||||
{t("login")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="block text-lg text-gray-800 dark:text-white"
|
||||
>
|
||||
{t("register")}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block text-left w-full text-lg text-red-600 hover:underline"
|
||||
>
|
||||
{t("logout")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="rounded-none p-4">
|
||||
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2">
|
||||
{t("subscribeTitle")}
|
||||
</h2>
|
||||
<Input type="email" placeholder={t("subscribePlaceholder")} />
|
||||
<Button className="bg-[#C6A455] mt-2">
|
||||
{t("subscribeButton")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 bg-black/50"
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
||||
{t("features")}
|
||||
</h3>
|
||||
<div className="space-y-5 ml-3">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
onClick={() => {
|
||||
if (item.label === t("forYou")) {
|
||||
if (!checkLoginStatus()) {
|
||||
router.push("/auth");
|
||||
} else {
|
||||
router.push("/for-you");
|
||||
}
|
||||
} else {
|
||||
router.push(item.href);
|
||||
}
|
||||
setIsSidebarOpen(false);
|
||||
}}
|
||||
className="block text-[15px] text-gray-800 text-left w-full"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 text-[16px] font-bold">
|
||||
<Link href="/about" className="block text-black">
|
||||
{t("about")}
|
||||
</Link>
|
||||
<Link href="/advertising" className="block text-black">
|
||||
{t("advertising")}
|
||||
</Link>
|
||||
<Link href="/contact" className="block text-black">
|
||||
{t("contact")}
|
||||
</Link>
|
||||
|
||||
{!isLoggedIn ? (
|
||||
<>
|
||||
<Link href="/auth" className="block text-lg text-gray-800">
|
||||
{t("login")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="block text-lg text-gray-800"
|
||||
>
|
||||
{t("register")}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block text-left w-full text-lg text-red-600 hover:underline"
|
||||
>
|
||||
{t("logout")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="rounded-none p-4">
|
||||
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2">
|
||||
{t("subscribeTitle")}
|
||||
</h2>
|
||||
<Input type="email" placeholder={t("subscribePlaceholder")} />
|
||||
<Button className="bg-[#C6A455] mt-2">
|
||||
{t("subscribeButton")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
</RevealT>
|
||||
|
||||
<div
|
||||
className="flex-1 bg-black/50"
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const Loader = () => {
|
|||
<div className="flex gap-2 items-center ">
|
||||
{/* <DashCodeLogo className=" text-default-900 h-8 w-8 [&>path:nth-child(3)]:text-background [&>path:nth-child(2)]:text-background" /> */}
|
||||
<Image
|
||||
src="/assets/logo1.png"
|
||||
src="/assets/mediahub-logo-min.png"
|
||||
alt=""
|
||||
width={80}
|
||||
height={80}
|
||||
|
|
|
|||
|
|
@ -93,17 +93,17 @@ export default function ImageDetail({ id }: { id: number }) {
|
|||
files:
|
||||
article.files?.map((f: any) => ({
|
||||
id: f.id,
|
||||
url: f.fileUrl,
|
||||
fileName: f.fileName,
|
||||
filePath: f.filePath,
|
||||
fileThumbnail: f.fileThumbnail,
|
||||
fileAlt: f.fileAlt,
|
||||
widthPixel: f.widthPixel,
|
||||
heightPixel: f.heightPixel,
|
||||
url: f.file_url,
|
||||
fileName: f.file_name,
|
||||
filePath: f.file_path,
|
||||
fileThumbnail: f.file_thumbnail,
|
||||
fileAlt: f.file_alt,
|
||||
widthPixel: f.width_pixel,
|
||||
heightPixel: f.height_pixel,
|
||||
size: f.size,
|
||||
downloadCount: f.downloadCount,
|
||||
createdAt: f.createdAt,
|
||||
updatedAt: f.updatedAt,
|
||||
downloadCount: f.download_count,
|
||||
createdAt: f.created_at,
|
||||
updatedAt: f.updated_at,
|
||||
})) || [],
|
||||
};
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ export default function ImageDetail({ id }: { id: number }) {
|
|||
<div className="relative">
|
||||
<Image
|
||||
placeholder={`data:image/svg+xml;base64,${toBase64(
|
||||
shimmer(700, 475),
|
||||
shimmer(700, 475)
|
||||
)}`}
|
||||
width={2560}
|
||||
height={1440}
|
||||
|
|
@ -163,8 +163,8 @@ export default function ImageDetail({ id }: { id: number }) {
|
|||
data?.files?.[selectedImage]?.url?.trim()
|
||||
? data.files[selectedImage].url
|
||||
: data?.thumbnailUrl?.trim()
|
||||
? data.thumbnailUrl
|
||||
: "/assets/empty-data.png"
|
||||
? data.thumbnailUrl
|
||||
: "/assets/empty-data.png"
|
||||
}
|
||||
alt="Main"
|
||||
className="rounded-lg h-[300px] w-screen lg:h-[600px] lg:w-full object-contain"
|
||||
|
|
@ -185,7 +185,7 @@ export default function ImageDetail({ id }: { id: number }) {
|
|||
<a onClick={() => setSelectedImage(index)} key={file?.id}>
|
||||
<Image
|
||||
placeholder={`data:image/svg+xml;base64,${toBase64(
|
||||
shimmer(700, 475),
|
||||
shimmer(700, 475)
|
||||
)}`}
|
||||
width={1920}
|
||||
height={1080}
|
||||
|
|
@ -223,7 +223,7 @@ export default function ImageDetail({ id }: { id: number }) {
|
|||
</span>
|
||||
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
|
||||
<Eye className="w-4 h-4" />
|
||||
{data.clickCount || 0}{" "}
|
||||
{data.viewCount || 0}
|
||||
</span>
|
||||
<span className="text-black">
|
||||
Creator: {data.creatorGroupLevelName}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export default function DashboardContainer() {
|
|||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4 p-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
{/* User Profile Card */}
|
||||
<motion.div
|
||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||
|
|
@ -135,11 +135,11 @@ export default function DashboardContainer() {
|
|||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-600 dark:text-black font-medium">Welcome back,</p>
|
||||
<p className="text-xl font-semibold text-gray-900 dark:text-black ">{username}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-black ">Admin Dashboard</p>
|
||||
<p className="text-gray-600 font-medium">Welcome back,</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{username}</p>
|
||||
<p className="text-sm text-gray-500">Admin Dashboard</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl dark:text-black ">
|
||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
||||
<DashboardUserIcon size={60} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -153,7 +153,7 @@ export default function DashboardContainer() {
|
|||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="p-3 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl dark:text-black">
|
||||
<div className="p-3 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl">
|
||||
<DashboardSpeecIcon />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -173,7 +173,7 @@ export default function DashboardContainer() {
|
|||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl dark:text-black">
|
||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl">
|
||||
<DashboardConnectIcon />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -193,7 +193,7 @@ export default function DashboardContainer() {
|
|||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="p-3 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl dark:text-black">
|
||||
<div className="p-3 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
|
||||
<DashboardShareIcon />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -213,7 +213,7 @@ export default function DashboardContainer() {
|
|||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl dark:text-black">
|
||||
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl">
|
||||
<DashboardCommentIcon size={40} />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -227,7 +227,7 @@ export default function DashboardContainer() {
|
|||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Analytics Chart */}
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4"
|
||||
|
|
@ -268,7 +268,7 @@ export default function DashboardContainer() {
|
|||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
{/* <div className="flex justify-between items-center mb-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Recent Content
|
||||
</h3>
|
||||
|
|
@ -277,7 +277,7 @@ export default function DashboardContainer() {
|
|||
Create Content
|
||||
</Button>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
|
||||
{article?.map((list: any) => (
|
||||
|
|
@ -306,12 +306,12 @@ export default function DashboardContainer() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* <div className="mt-6 flex justify-center">
|
||||
<div className="mt-6 flex justify-center">
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -113,12 +113,12 @@ export function WorkflowModalProvider({ children }: WorkflowModalProviderProps)
|
|||
return (
|
||||
<WorkflowModalContext.Provider value={{ showWorkflowModal, hideWorkflowModal, refreshWorkflowStatus }}>
|
||||
{children}
|
||||
<WorkflowSetupModal
|
||||
{/* <WorkflowSetupModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={hideWorkflowModal}
|
||||
workflowInfo={workflowInfo}
|
||||
onRefresh={refreshWorkflowStatus}
|
||||
/>
|
||||
/> */}
|
||||
</WorkflowModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { IconX, SettingsIcon } from "@/components/icons";
|
||||
|
|
@ -25,12 +20,7 @@ interface WorkflowSetupModalProps {
|
|||
onRefresh?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function WorkflowSetupModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
workflowInfo,
|
||||
onRefresh,
|
||||
}: WorkflowSetupModalProps) {
|
||||
export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo, onRefresh }: WorkflowSetupModalProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
|
@ -43,11 +33,7 @@ export default function WorkflowSetupModal({
|
|||
|
||||
const handleClose = () => {
|
||||
// Allow closing if workflow is setup OR if user is on tenant settings page
|
||||
if (
|
||||
workflowInfo?.hasWorkflowSetup ||
|
||||
pathname?.includes("/admin/settings/tenant") ||
|
||||
pathname?.includes("/tenant")
|
||||
) {
|
||||
if (workflowInfo?.hasWorkflowSetup || pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
|
|
@ -66,33 +52,19 @@ export default function WorkflowSetupModal({
|
|||
return (
|
||||
<Dialog
|
||||
open={isVisible}
|
||||
onOpenChange={
|
||||
workflowInfo?.hasWorkflowSetup ||
|
||||
pathname?.includes("/admin/settings/tenant") ||
|
||||
pathname?.includes("/tenant")
|
||||
? handleClose
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={(workflowInfo?.hasWorkflowSetup || pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) ? handleClose : undefined}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-w-md"
|
||||
onPointerDownOutside={(e) => {
|
||||
// Prevent closing by clicking outside unless workflow is setup or on tenant settings page
|
||||
if (
|
||||
!workflowInfo?.hasWorkflowSetup &&
|
||||
!pathname?.includes("/admin/settings/tenant") &&
|
||||
!pathname?.includes("/tenant")
|
||||
) {
|
||||
if (!workflowInfo?.hasWorkflowSetup && !pathname?.includes('/admin/settings/tenant') && !pathname?.includes('/tenant')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onEscapeKeyDown={(e) => {
|
||||
// Prevent closing by pressing ESC unless workflow is setup or on tenant settings page
|
||||
if (
|
||||
!workflowInfo?.hasWorkflowSetup &&
|
||||
!pathname?.includes("/admin/settings/tenant") &&
|
||||
!pathname?.includes("/tenant")
|
||||
) {
|
||||
if (!workflowInfo?.hasWorkflowSetup && !pathname?.includes('/admin/settings/tenant') && !pathname?.includes('/tenant')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
|
@ -113,139 +85,46 @@ export default function WorkflowSetupModal({
|
|||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{
|
||||
!workflowInfo?.hasWorkflowSetup ? (
|
||||
// No Workflow Setup
|
||||
<Card className="border-orange-200 bg-orange-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-6 w-6 rounded-full bg-orange-600 flex items-center justify-center mt-1">
|
||||
<span className="text-white text-sm">!</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-orange-900 mb-2">
|
||||
Workflow Belum Dikonfigurasi
|
||||
</h3>
|
||||
<p className="text-sm text-orange-700 mb-4">
|
||||
Anda belum melakukan setup workflow, silahkan setup
|
||||
terlebih dahulu.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSetupWorkflow}
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||
size="sm"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4 mr-2" />
|
||||
Setup Workflow
|
||||
</Button>
|
||||
{(pathname?.includes("/admin/settings/tenant") ||
|
||||
pathname?.includes("/tenant")) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!workflowInfo?.hasWorkflowSetup ? (
|
||||
// No Workflow Setup
|
||||
<Card className="border-orange-200 bg-orange-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-6 w-6 rounded-full bg-orange-600 flex items-center justify-center mt-1">
|
||||
<span className="text-white text-sm">!</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="border-green-200 bg-green-50">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-6 w-6 rounded-full bg-green-600 flex items-center justify-center mt-1">
|
||||
<span className="text-white text-sm">✓</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-green-900 mb-3">
|
||||
Workflow Sudah Dikonfigurasi
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">
|
||||
Default Workflow
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{workflowInfo.defaultWorkflowName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Workflow ID</span>
|
||||
<span className="font-mono">
|
||||
#{workflowInfo.defaultWorkflowId}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">
|
||||
Requires Approval
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
workflowInfo.requiresApproval
|
||||
? "text-green-600 font-medium"
|
||||
: "text-gray-500"
|
||||
}
|
||||
>
|
||||
{workflowInfo.requiresApproval ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Auto Publish</span>
|
||||
<span
|
||||
className={
|
||||
workflowInfo.autoPublishArticles
|
||||
? "text-green-600 font-medium"
|
||||
: "text-gray-500"
|
||||
}
|
||||
>
|
||||
{workflowInfo.autoPublishArticles ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Approval Status</span>
|
||||
<span
|
||||
className={
|
||||
workflowInfo.isApprovalActive
|
||||
? "text-green-600 font-medium"
|
||||
: "text-red-600 font-medium"
|
||||
}
|
||||
>
|
||||
{workflowInfo.isApprovalActive
|
||||
? "Active"
|
||||
: "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex gap-2 mt-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-orange-900 mb-2">
|
||||
Workflow Belum Dikonfigurasi
|
||||
</h3>
|
||||
<p className="text-sm text-orange-700 mb-4">
|
||||
Anda belum melakukan setup workflow, silahkan setup terlebih dahulu.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSetupWorkflow}
|
||||
variant="outline"
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||
size="sm"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4 mr-2" />
|
||||
Manage Workflow
|
||||
Setup Workflow
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={handleClose} size="sm">
|
||||
Close
|
||||
</Button>
|
||||
</div> */}
|
||||
{(pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
size="sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
// Workflow Setup Complete
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : ''
|
||||
// Workflow Setup Complete
|
||||
// <Card className="border-green-200 bg-green-50">
|
||||
// <CardContent className="p-4">
|
||||
// <div className="flex items-start gap-3">
|
||||
|
|
@ -294,8 +173,6 @@ export default function WorkflowSetupModal({
|
|||
// </div>
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
)
|
||||
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ const columns: ColumnDef<any>[] = [
|
|||
<span className="normal-case">{row.getValue("userLevelGroup") || "-"}</span>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// accessorKey: "statusId",
|
||||
// header: "Status ID",
|
||||
// cell: ({ row }) => (
|
||||
// <span>{row.getValue("statusId")}</span>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorKey: "statusId",
|
||||
header: "Status ID",
|
||||
cell: ({ row }) => (
|
||||
<span>{row.getValue("statusId")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Tanggal Unggah",
|
||||
|
|
@ -143,14 +143,14 @@ const columns: ColumnDef<any>[] = [
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-slate-200 focus:text-blue-500 rounded-none">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/admin/management-user/detail/${row.original.id}`}
|
||||
>
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-slate-200 focus:text-blue-500 rounded-none">
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Link
|
||||
href={`/admin/management-user/edit/${row.original.id}`}
|
||||
>
|
||||
|
|
@ -159,7 +159,7 @@ const columns: ColumnDef<any>[] = [
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
color="red"
|
||||
className="p-2 border-b text-red-500 group focus:bg-red-500 focus:text-white rounded-none cursor-pointer"
|
||||
className="p-2 border-b text-red-500 group focus:bg-red-500 focus:text-white rounded-none"
|
||||
>
|
||||
<a
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ const UserInternalTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-default-50">
|
||||
<>
|
||||
<div className="flex justify-between py-3">
|
||||
<Input
|
||||
type="text"
|
||||
|
|
@ -396,7 +396,7 @@ const UserInternalTable = () => {
|
|||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue