Compare commits

...

46 Commits

Author SHA1 Message Date
Sabda Yagra 9223122e66 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-27 10:48:33 +07:00
Sabda Yagra 9e286ffd06 fix: dev fixing
continuous-integration/drone/push Build is passing Details
2026-02-27 10:47:55 +07:00
Sabda Yagra 18d6f32536 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 15:29:26 +07:00
Sabda Yagra 9c2b3612c6 fix: ck editor
continuous-integration/drone/push Build is passing Details
2026-02-20 15:16:23 +07:00
Sabda Yagra 6bd83237c4 fix: install all package and fixing task 2026-02-20 15:13:48 +07:00
Sabda Yagra 61df236e13 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 14:09:25 +07:00
Sabda Yagra 7f9166b866 fix: .drone.yml
continuous-integration/drone/push Build is passing Details
2026-02-20 14:04:40 +07:00
Sabda Yagra 2876e4eb30 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 13:55:41 +07:00
Sabda Yagra 292cde6ed0 feat: drone.yml
continuous-integration/drone/push Build is passing Details
2026-02-20 11:55:18 +07:00
Sabda Yagra 3c5e644668 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 22:47:17 +07:00
Sabda Yagra d80adaa133 fix: schedule checkbox 2026-02-11 22:46:47 +07:00
Sabda Yagra eefa43a5b0 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 18:13:06 +07:00
Sabda Yagra 277f1cc805 fix: api key maps google 2026-02-11 18:10:36 +07:00
Sabda Yagra f357a4e8ce fix: api key maps 2026-02-11 18:09:39 +07:00
Sabda Yagra bb91379058 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 17:13:44 +07:00
Sabda Yagra 740d73d689 fix: satker section all and maps 2026-02-11 17:12:50 +07:00
Sabda Yagra 76c4ed9238 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-29 18:39:52 +07:00
Sabda Yagra 44c6cc6d9d fix: menus for polres 2026-01-29 18:38:34 +07:00
Sabda Yagra 2b8f7b724e Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-22 14:28:34 +07:00
Sabda Yagra 10694547d1 fix: table task mabes=>koorkurator 2026-01-22 14:24:33 +07:00
Sabda Yagra e5e32496ab Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-22 12:03:33 +07:00
Sabda Yagra 78d1fc0d93 fix: schedule live report 2026-01-22 11:59:48 +07:00
Sabda Yagra c99e1a5c7d fix: penugasan mabes=>koorkurator 2026-01-21 10:06:40 +07:00
Sabda Yagra 274fd022e1 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-13 09:55:09 +07:00
Sabda Yagra 933a672280 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-13 09:53:07 +07:00
Sabda Yagra ce7c343808 fix: taskTA from mabes to koorkurator 2026-01-13 09:52:29 +07:00
hanif salafi cba94cf0a9 feat: rollback using pnpm 2026-01-13 02:20:57 +07:00
Sabda Yagra 7c721dcc96 fix: form create task-ta 2026-01-12 09:09:52 +07:00
Sabda Yagra 78ddd461df Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-09 20:33:10 +07:00
Sabda Yagra ec2d7eb203 fixing 2026-01-09 20:32:39 +07:00
Sabda Yagra ad5d048e78 fixing 2026-01-09 20:27:20 +07:00
Sabda Yagra 5614b5ada5 fixing 2026-01-09 20:08:13 +07:00
Sabda Yagra f1b59ee537 fix: add xlsx and file-saver dependency 2026-01-09 18:47:26 +07:00
Sabda Yagra 0ad1acee09 fix: button relevant in media tracking and table 2026-01-09 18:35:20 +07:00
Sabda Yagra 2d09af2e8b fix: local dev error 2026-01-09 14:30:51 +07:00
Sabda Yagra edafc223db fix: baseUrl api 2026-01-09 11:11:55 +07:00
Sabda Yagra 933cdb1100 fix: button validate in detail media tracking 2026-01-09 10:22:43 +07:00
Sabda Yagra 280ea508e9 fix: adjust coloumns media tracking 2026-01-06 11:22:13 +07:00
Sabda Yagra 1a0d53bb11 fix: new content typeId 2025-12-31 13:20:22 +07:00
Sabda Yagra be1c799629 fix: change docker 2025-12-29 08:37:26 +07:00
Sabda Yagra 3d23d75e05 fix: change ip docker image 2025-12-26 16:14:10 +07:00
Sabda Yagra 1ee379df71 fix: change docker and jenkins IP 2025-12-24 12:18:41 +07:00
Sabda Yagra 9a1b41e90b fix: change api url 2025-12-17 15:17:01 +07:00
Sabda Yagra fcfa24b06c fix: change ip jenkins 2025-12-17 11:22:20 +07:00
Sabda Yagra cf91ea33ec fix: adjust gitlab ci 2025-12-15 09:31:24 +07:00
Sabda Yagra 1cc7786dee fix: change gitlab ci 2025-12-15 08:36:49 +07:00
53 changed files with 3727 additions and 1970 deletions

45
.drone.yml Normal file
View File

@ -0,0 +1,45 @@
kind: pipeline
type: ssh
name: mediahub-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-sabda-v2
- main
- prod
commands:
- rm -rf /opt/build/mediahub-fe
- mkdir -p /opt/build
- cd /opt/build
- git clone http://38.47.180.165:3000/mediahub/mediahub-fe.git
- name: build image
when:
branch:
- dev-sabda-v2
- prod
commands:
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
- cd /opt/build/mediahub-fe
- docker build -t 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH .
- docker push 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
- name: deploy
when:
branch:
- prod
commands:
- docker pull 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
- docker stop new-mediahub-fe || true
- docker rm new-mediahub-fe || true
- docker run -dt -p 4200:3000 --restart always --name new-mediahub-fe 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH

4
.env
View File

@ -1,3 +1,3 @@
NEXT_PUBLIC_API=https://netidhub.com/api
NEXT_PUBLIC=https://netidhub.com
NEXT_PUBLIC_API=https://new.netidhub.com/api
NEXT_PUBLIC=https://new.netidhub.com
NEXT_PUBLIC_TINYMCE_API_KEY=bhteuja26yz5p0aubxry9b95hs33amgn65kjv5km0fd5iuev

View File

@ -1,23 +1,23 @@
stages:
- build
- deploy
build-dev:
stage: build
when: on_success
only:
- main
- dev-landing-v2
image:
image:
name: docker:25.0.3-cli
services:
- name: docker:25.0.3-dind
command: ["--insecure-registry=103.82.242.92:8900"]
command: ["--insecure-registry=38.47.185.86:8900"]
script:
- docker logout
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker build -t 103.82.242.92:8900/mediahub/new-mediahub-fe:dev .
- docker push 103.82.242.92:8900/mediahub/new-mediahub-fe:dev
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
- docker build -t 38.47.185.86:8900/mediahub/new-mediahub-fe:dev .
- docker push 38.47.185.86:8900/mediahub/new-mediahub-fe:dev
auto-deploy:
stage: deploy
@ -26,7 +26,5 @@ auto-deploy:
- main
- dev-landing-v2
image: curlimages/curl:latest
services:
- docker:dind
script:
- curl --user admin:$JENKINS_PWD http://103.31.38.120:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub

View File

@ -31,6 +31,8 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import { validateMediaLink } from "@/service/media-tracking/media-tracking";
import toast from "react-hot-toast";
const columns: ColumnDef<any>[] = [
{
@ -52,12 +54,132 @@ const columns: ColumnDef<any>[] = [
<span className="normal-case">{row.getValue("title")}</span>
),
},
// {
// accessorKey: "link",
// header: "Link Berita",
// cell: ({ row }) => (
// <span className="normal-case">{row.getValue("link")}</span>
// ),
// },
{
accessorKey: "link",
header: "Link Berita",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("link")}</span>
),
cell: ({ row }) => {
const link = row.getValue<string>("link");
if (!link) {
return <span className="text-muted-foreground">-</span>;
}
return (
<Link
href={link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800 break-all"
>
{link}
</Link>
);
},
},
{
id: "validation",
header: "Validasi",
cell: ({ row, table }) => {
const original = row.original;
// const isValid = original.isValid;
const isRelevant = original.isRelevant;
const link = original.link;
const updateRow = (data: Partial<any>) => {
table.options.meta?.updateData(row.index, data);
};
const handleValid = async () => {
try {
await validateMediaLink(original.id, true);
updateRow({
isRelevant: true,
});
table.options.meta?.refetchData?.();
} catch (err: any) {
toast.error(err.message);
}
};
const handleInvalid = async () => {
try {
await validateMediaLink(original.id, false);
updateRow({
isRelevant: false,
});
table.options.meta?.refetchData?.();
} catch (err: any) {
toast.error(err.message);
}
};
if (!link) {
return <span className="text-muted-foreground">-</span>;
}
if (isRelevant === true) {
return (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
disabled
>
Relevan
</Button>
);
}
return (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleValid}
className="flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M18.7 7.2c-.4-.4-1-.4-1.4 0l-7.5 7.5l-3.1-3.1c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4l3.8 3.8c.2.2.4.3.7.3s.5-.1.7-.3l8.2-8.2c.4-.4.4-1 0-1.4"
/>
</svg>
Relevan
</Button>
<Button size="sm" variant="outline" onClick={handleInvalid} className="flex text-center items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M6.758 17.243L12.001 12m5.243-5.243L12 12m0 0L6.758 6.757M12.001 12l5.243 5.243"
/>
</svg>
Tidak Relevan
</Button>
</div>
);
},
},
];

View File

@ -116,6 +116,18 @@ const NewsDetailTable = () => {
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
meta: {
updateData: (rowIndex: number, value: Partial<any>) => {
setDataTable((old) =>
old.map((row, index) =>
index === rowIndex ? { ...row, ...value } : row
)
);
},
refetchData: () => {
fetchData();
},
},
state: {
sorting,
columnFilters,
@ -154,7 +166,7 @@ const NewsDetailTable = () => {
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
}, [page, showData, id]);
async function fetchData() {
try {

View File

@ -1,7 +1,15 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { exportMediaTrackingToExcel } from "@/utils/export-media-tracking";
import { loading, close } from "@/config/swal";
import { error } from "@/lib/swal";
import {
DownloadIcon,
Eye,
MoreVertical,
SquarePen,
Trash2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
@ -46,7 +54,7 @@ const columns: ColumnDef<any>[] = [
},
{
accessorKey: "resultTotal",
header: () => <div className="text-center w-full">Jumlah Amplifikasi</div>,
header: () => <div className="text-center w-full">Total Artikel</div>,
cell: ({ row }) => {
const value = row.getValue("resultTotal") as number | string | null;
@ -58,6 +66,29 @@ const columns: ColumnDef<any>[] = [
return <div className="text-center w-full">{finalValue}</div>;
},
},
{
accessorKey: "amplification",
header: () => <div className="text-center w-full">Jumlah Amplifikasi</div>,
cell: ({ row }) => {
const raw = row.getValue("amplification") as string | null;
let total = 0;
let invalidTotal = 0;
if (raw && typeof raw === "string") {
const parts = raw.split("/").map((v) => v.trim());
total = Number(parts[0]) || 0;
invalidTotal = Number(parts[1]) || 0;
}
return (
<div className="text-center w-full font-medium">
{total}
<span className="text-muted-foreground">/{invalidTotal}</span>
</div>
);
},
},
// {
// accessorKey: "status",
@ -81,11 +112,7 @@ const columns: ColumnDef<any>[] = [
header: () => <div className="text-center">Status</div>,
cell: ({ row }) => {
const raw = Boolean(row.getValue("isProcessing"));
// KONDISI STATUS
const statusText = raw ? "Sedang Diproses" : "Sudah Selesai";
// WARNA STATUS
const colorClass = raw
? "bg-yellow-100 text-yellow-700 border border-yellow-300"
: "bg-green-100 text-green-700 border border-green-300";
@ -141,13 +168,31 @@ const columns: ColumnDef<any>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/media-tracking/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground items-center rounded-none">
<DropdownMenuItem className="p-2 border-b cursor-pointer text-default-700 group focus:bg-default focus:text-primary-foreground items-center rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View&nbsp;
{row.original.mediaUpload.fileType.secondaryName &&
row.original.mediaUpload.fileType.secondaryName.toLowerCase()}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
className="p-2 border-b cursor-pointer text-default-700 group rounded-none focus:bg-default focus:text-primary-foreground "
onClick={async () => {
try {
loading();
await exportMediaTrackingToExcel({
mediaTrackingId: row.original.id,
});
close();
} catch (e: any) {
close();
error(e.message || "Gagal export data");
}
}}
>
<DownloadIcon className="w-4 h-4 me-1.5" />
<p>Download</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@ -894,7 +894,6 @@ const EventModal = ({
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
console.log("CSRF TOKEN : ", csrfToken);
const headers = {
"X-XSRF-TOKEN": csrfToken,
};

View File

@ -153,7 +153,7 @@ const useTableColumns = ({
// try {
// loading();
// const response = await axios.get(
// `https://netidhub.com/api/media/report/download?id=${id}`,
// `https://new.netidhub.com/api/media/report/download?id=${id}`,
// {
// responseType: "blob",
// }

View File

@ -22,7 +22,9 @@ import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useTranslations } from "next-intl";
const useTableColumns = (activeTab: "ta" | "daily" | "special") => {
const useTableColumns = (
activeTab: "ta" | "daily" | "special" | "mabes-koor",
) => {
const t = useTranslations("Table");
const columns: ColumnDef<any>[] = [
{
@ -190,7 +192,11 @@ const useTableColumns = (activeTab: "ta" | "daily" | "special") => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
{(roleId == 11 || roleId == 12 || roleId == 19) && (
{/* {(roleId == 11 || roleId == 12 || roleId == 19) && ( */}
{(roleId == 11 ||
roleId == 12 ||
roleId == 19 ||
roleId == 3) && (
<Link href={`/contributor/task-ta/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
@ -198,14 +204,15 @@ const useTableColumns = (activeTab: "ta" | "daily" | "special") => {
</DropdownMenuItem>
</Link>
)}
{roleId == 11 && (
<Link href={`/contributor/task-ta/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
)}
{roleId == 11 ||
(roleId == 3 && (
<Link href={`/contributor/task-ta/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
))}
{(roleId == 12 || roleId == 19) && (
<Link
href={`/contributor/task-ta/upload-task/${row.original.id}`}
@ -216,15 +223,16 @@ const useTableColumns = (activeTab: "ta" | "daily" | "special") => {
</DropdownMenuItem>
</Link>
)}
{roleId == 11 && (
<DropdownMenuItem
onClick={() => TaskDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
)}
{roleId == 11 ||
(roleId == 3 && (
<DropdownMenuItem
onClick={() => TaskDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);

View File

@ -5,7 +5,11 @@ import { Button } from "@/components/ui/button";
import { UploadIcon } from "lucide-react";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Link } from "@/components/navigation";
import { checkAuthorization, checkLoginSession } from "@/lib/utils";
import {
checkAuthorization,
checkLoginSession,
getCookiesDecrypt,
} from "@/lib/utils";
import React, { useEffect } from "react";
import { useTranslations } from "next-intl";
@ -13,12 +17,13 @@ const TaskPage = () => {
const t = useTranslations("AnalyticsDashboard");
useEffect(() => {
function initState() {
checkAuthorization("admin");
checkAuthorization("admin");
checkLoginSession();
}
initState();
}, []);
const levelNumber = getCookiesDecrypt("ulne");
return (
<div>
@ -29,16 +34,28 @@ const TaskPage = () => {
<CardTitle>
<div className="flex flex-col sm:flex-row lg:flex-row lg:items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("tabel", { defaultValue: "Tabel" })} {t("task", { defaultValue: "Task" })}
{t("tabel", { defaultValue: "Tabel" })}{" "}
{t("task", { defaultValue: "Task" })}
</div>
<div className="flex-none">
{Number(levelNumber) !== 3 && (
<div className="flex-none">
<Link href="/contributor/task/create">
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-task", { defaultValue: "Create Task" })}
</Button>
</Link>
</div>
)}
{/* <div className="flex-none">
<Link href={"/contributor/task/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-task", { defaultValue: "Create Task" })}
</Button>
</Link>
</div>
</div> */}
</div>
</CardTitle>
</CardHeader>

View File

@ -24,6 +24,7 @@ import ReportTable from "../contributor/report/components/report-table";
const DashboardPage = () => {
const t = useTranslations("AnalyticsDashboard");
const roleId = getCookiesDecrypt("urie");
const levelNumber = Number(getCookiesDecrypt("ulne"));
return Number(roleId) == 2 || Number(roleId) == 11 || Number(roleId) == 12 ? (
<div>
@ -54,18 +55,23 @@ const DashboardPage = () => {
>
{t("schedule", { defaultValue: "Schedule" })}
</TabsTrigger>
<TabsTrigger
value="indeks"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
{t("indeks", { defaultValue: "Indeks" })}
</TabsTrigger>
<TabsTrigger
value="report"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
{t("report", { defaultValue: "Report" })}
</TabsTrigger>
{levelNumber !== 3 && (
<>
<TabsTrigger
value="indeks"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
{t("indeks", { defaultValue: "Indeks" })}
</TabsTrigger>
<TabsTrigger
value="report"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
{t("report", { defaultValue: "Report" })}
</TabsTrigger>
</>
)}
</TabsList>
</Card>
<TabsContent value="routine-task">
@ -75,18 +81,24 @@ const DashboardPage = () => {
<CardContent className="p-4">
<div className="grid md:grid-cols-3 gap-4">
<StatisticsBlock
title={t("Hasil_unggah_disetujui_hari_ini", { defaultValue: "Hasil Unggah Disetujui Hari Ini" })}
title={t("Hasil_unggah_disetujui_hari_ini", {
defaultValue: "Hasil Unggah Disetujui Hari Ini",
})}
total="3,564"
className="bg-info/10 border-none shadow-none"
/>
<StatisticsBlock
title={t("Hasil_unggah_direvisi_hari_ini", { defaultValue: "Hasil Unggah Direvisi Hari Ini" })}
title={t("Hasil_unggah_direvisi_hari_ini", {
defaultValue: "Hasil Unggah Direvisi Hari Ini",
})}
total="564"
className="bg-warning/10 border-none shadow-none"
chartColor="#FB8F65"
/>
<StatisticsBlock
title={t("Hasil_unggah_ditolak_hari_ini", { defaultValue: "Hasil Unggah Ditolak Hari Ini" })}
title={t("Hasil_unggah_ditolak_hari_ini", {
defaultValue: "Hasil Unggah Ditolak Hari Ini",
})}
total="+5.0%"
className="bg-primary/10 border-none shadow-none"
chartColor="#2563eb"
@ -101,7 +113,9 @@ const DashboardPage = () => {
<Card>
<CardHeader className="flex flex-row items-center">
<CardTitle className="flex-1 text-lg">
{t("Total-Content-Production", { defaultValue: "Total Content Production" })}
{t("Total-Content-Production", {
defaultValue: "Total Content Production",
})}
</CardTitle>
<DashboardDropdown />
</CardHeader>
@ -113,7 +127,9 @@ const DashboardPage = () => {
<div className="lg:col-span-8 col-span-12">
<Card>
<CardHeader className="flex flex-row items-center">
<CardTitle className="flex-1">{t("tabel", { defaultValue: "Tabel" })}</CardTitle>
<CardTitle className="flex-1">
{t("tabel", { defaultValue: "Tabel" })}
</CardTitle>
{/* <DashboardDropdown /> */}
</CardHeader>
<CardContent className="p-0">
@ -132,14 +148,26 @@ const DashboardPage = () => {
<div className="flex-1 text-xl font-medium text-default-900">
Tabel Penugasan
</div>
<div>
{Number(levelNumber) !== 3 && (
<div className="flex-none">
<Link href="/contributor/task/create">
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-task", {
defaultValue: "Create Task",
})}
</Button>
</Link>
</div>
)}
{/* <div>
<Link href={"/contributor/task/create"}>
<Button color="primary" className="text-white">
<UploadIcon />
Buat Penugasan
</Button>
</Link>
</div>
</div> */}
</div>
</Card>
<CardContent className="p-0 mt-3">

View File

@ -13,6 +13,8 @@ import { useTranslations } from "next-intl";
const ContestPage = () => {
const [userLevelId, setUserLevelId] = useState<any>(null);
const t = useTranslations("Contest");
const levelNumber = Number(getCookiesDecrypt("ulne"));
useEffect(() => {
setUserLevelId(Number(getCookiesDecrypt("ulie")));
}, []);
@ -25,18 +27,36 @@ const ContestPage = () => {
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("table", { defaultValue: "Table" })} {t("contest", { defaultValue: "Contest" })}
{t("table", { defaultValue: "Table" })}{" "}
{t("contest", { defaultValue: "Contest" })}
</div>
{userLevelId !== 776 && userLevelId !== null && (
{userLevelId !== 776 &&
userLevelId !== null &&
levelNumber !== 3 && (
<div className="flex-none">
<Link href={"/shared/contest/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-contest", {
defaultValue: "Create Contest",
})}
</Button>
</Link>
</div>
)}
{/* {userLevelId !== 776 && userLevelId !== null && (
<div className="flex-none">
<Link href={"/shared/contest/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-contest", { defaultValue: "Create Contest" })}
{t("create-contest", {
defaultValue: "Create Contest",
})}
</Button>
</Link>
</div>
)}
)} */}
</div>
</CardTitle>
</CardHeader>

View File

@ -85,7 +85,7 @@ const LatestNews = (props: { type: string }) => {
// useEffect(() => {
// async function fetchCategories() {
// const url = "https://netidhub.com/api/csrf";
// const url = "https://new.netidhub.com/api/csrf";
// try {
// const response = await fetch(url);

View File

@ -82,7 +82,7 @@ const NationalNews = () => {
// useEffect(() => {
// async function fetchCategories() {
// const url = "https://netidhub.com/api/csrf";
// const url = "https://new.netidhub.com/api/csrf";
// try {
// const response = await fetch(url);

View File

@ -23,7 +23,7 @@ const PopularNews = () => {
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);

View File

@ -75,7 +75,7 @@ const RegionalNews = () => {
// useEffect(() => {
// async function fetchCategories() {
// const url = "https://netidhub.com/api/csrf";
// const url = "https://new.netidhub.com/api/csrf";
// try {
// const response = await fetch(url);

View File

@ -311,14 +311,15 @@ export default function FormQuestionsForward() {
{` `}
mengirimkan pesan untuk{` `}
<Link
href={
detail?.feed
? detail?.feed?.permalink_url == undefined
? detail?.feedUrl
: detail?.feed?.permalink_url
: ""
}
target="_blank"
// href={
// detail?.feed
// ? detail?.feed?.permalink_url == undefined
// ? detail?.feedUrl
// : detail?.feed?.permalink_url
// : ""
// }
// target="_blank"
href={detail?.feedUrl}
className="font-bold"
>
{detail?.message}

View File

@ -115,7 +115,7 @@ const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
{ ssr: false },
);
interface Destination {
@ -198,6 +198,12 @@ export default function FormAudioDetail() {
satker: boolean;
}>
>([]);
const [creatorLevelNumber, setCreatorLevelNumber] = useState<number | null>(
null,
);
const isContentFromSatker = creatorLevelNumber === 3;
const isContentFromMabesOrPolda =
creatorLevelNumber === 1 || creatorLevelNumber === 2;
useEffect(() => {
if (Number(userLevelId) === 216 && Number(roleId) === 3) {
@ -209,7 +215,7 @@ export default function FormAudioDetail() {
const handleFileUnitChange = (
fileIndex: number,
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
setFileUnitSelections((prev) => {
const newSelections = [...prev];
@ -229,7 +235,7 @@ export default function FormAudioDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[fileIndex] || new Set()
newArray[fileIndex] || new Set(),
);
if (value) {
@ -260,12 +266,12 @@ export default function FormAudioDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
currentFileCheckedLevels.has(Number(item.id))
currentFileCheckedLevels.has(Number(item.id)),
);
if (!hasSelectedPolda) {
alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES.",
);
return prev; // Batalkan perubahan
}
@ -302,7 +308,7 @@ export default function FormAudioDetail() {
useEffect(() => {
if (detail?.assignedToTopLevel) {
const outputSet = new Set(
detail.assignedToTopLevel.split(",").map(Number)
detail.assignedToTopLevel.split(",").map(Number),
);
setUnitSelection({
semua: outputSet.has(0),
@ -327,7 +333,7 @@ export default function FormAudioDetail() {
acc[polda.id] = false;
return acc;
},
{}
{},
);
setExpandedPolda(initialExpandedState);
console.log("polres", initialExpandedState);
@ -362,7 +368,7 @@ export default function FormAudioDetail() {
const handleUnitChange = (
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
if (key === "semua") {
const newState = {
@ -472,7 +478,7 @@ export default function FormAudioDetail() {
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
};
@ -494,7 +500,7 @@ export default function FormAudioDetail() {
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
o.name.toLowerCase().includes("pers rilis"),
);
if (findCategory) {
@ -542,6 +548,9 @@ export default function FormAudioDetail() {
const details = response?.data?.data;
setFiles(details?.files);
setDetail(details);
if (details?.uploadedBy?.userLevel?.levelNumber) {
setCreatorLevelNumber(details.uploadedBy.userLevel.levelNumber);
}
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
@ -552,14 +561,14 @@ export default function FormAudioDetail() {
if (details?.assignedToLevel) {
const levels = new Set(
details.assignedToLevel.split(",").map(Number)
details.assignedToLevel.split(",").map(Number),
);
setCheckedLevels(levels);
}
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id
(obj: any) => obj.id,
);
setSelectedPublishers(publisherIds);
}
@ -709,7 +718,7 @@ export default function FormAudioDetail() {
const setupPlacement = (
index: number,
placement: string,
checked: boolean
checked: boolean,
) => {
let temp = [...filePlacements];
if (checked) {
@ -720,7 +729,7 @@ export default function FormAudioDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Checklist semua item di modal
@ -770,7 +779,7 @@ export default function FormAudioDetail() {
// Hanya auto-checklist "all" jika polda, polres, dan mabes ter-checklist
// JANGAN include satker dalam perhitungan auto "all"
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length === 3 && !now.includes("all")) {
now.push("all");
@ -785,7 +794,7 @@ export default function FormAudioDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Unchecklist semua item di modal
@ -819,7 +828,7 @@ export default function FormAudioDetail() {
// Hapus "all" jika tidak semua item ter-checklist
if (now.includes("all")) {
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length < 3) {
const newData = now.filter((b) => b !== "all");
@ -838,7 +847,7 @@ export default function FormAudioDetail() {
type: string,
url: string,
names: string,
format: string
format: string,
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
@ -873,7 +882,7 @@ export default function FormAudioDetail() {
const updateModalChecklistLevels = (
fileIndex: number,
placement: string,
checked: boolean
checked: boolean,
) => {
if (!listDest || listDest.length === 0) return;
@ -906,7 +915,7 @@ export default function FormAudioDetail() {
} else if (placement === "satker") {
// Checklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.add(Number(satkerItem.id));
@ -942,7 +951,7 @@ export default function FormAudioDetail() {
} else if (placement === "satker") {
// Unchecklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.delete(Number(satkerItem.id));
@ -975,7 +984,7 @@ export default function FormAudioDetail() {
// Fungsi untuk mengupdate checklist levels untuk file tertentu
const handleFileCheckboxChangePlacement = (
fileIndex: number,
levelId: number
levelId: number,
) => {
setFileCheckedLevels((prev) => {
const newArray = [...prev];
@ -987,7 +996,7 @@ export default function FormAudioDetail() {
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (
poldaItem &&
@ -1010,7 +1019,7 @@ export default function FormAudioDetail() {
// Jika ini adalah SATKER POLRI yang di-checklist, checklist juga semua sub-item di bawahnya
const satkerItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (satkerItem && satkerItem.name === "SATKER POLRI") {
// Checklist semua sub-item di bawah SATKER POLRI (bukan POLRES)
@ -1041,7 +1050,7 @@ export default function FormAudioDetail() {
// Hitung total POLDA yang ada (bukan SATKER POLRI)
const totalPoldaCount = listDest.filter(
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI"
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI",
).length;
// Hitung berapa banyak POLDA yang ter-checklist (bukan SATKER POLRI)
@ -1068,7 +1077,7 @@ export default function FormAudioDetail() {
}
return total;
},
0
0,
);
// Hitung berapa banyak POLRES yang ter-checklist
@ -1078,7 +1087,7 @@ export default function FormAudioDetail() {
return (
total +
item.subDestination.filter((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
).length
);
}
@ -1087,7 +1096,7 @@ export default function FormAudioDetail() {
// Cek apakah SATKER POLRI ter-checklist
const satkerItem = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
const isSatkerChecked =
satkerItem && currentFileLevels.has(Number(satkerItem.id));
@ -1121,7 +1130,7 @@ export default function FormAudioDetail() {
// Cek apakah semua sub-items sudah ter-checklist
const allSubItemsChecked = polda.subDestination?.every((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
);
if (allSubItemsChecked) {
@ -1204,7 +1213,7 @@ export default function FormAudioDetail() {
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
String(cat.id) === String(detail.category.id),
) && (
<SelectItem
key={String(detail.category.id)}
@ -1464,7 +1473,7 @@ export default function FormAudioDetail() {
Tingkat Distribusi:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{/* {[
{ key: "semua", label: "Semua" },
{ key: "nasional", label: "Nasional" },
{ key: "wilayah", label: "Wilayah" },
@ -1472,6 +1481,39 @@ export default function FormAudioDetail() {
key: "international",
label: "Internasional",
},
] */}
{[
{ key: "semua", label: "Semua" },
...(isContentFromMabesOrPolda
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
{
key: "wilayah",
label: "Wilayah",
},
]
: []),
...(isContentFromSatker
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
]
: []),
].map((item, idx) => (
<div
key={item.key}
@ -1488,12 +1530,12 @@ export default function FormAudioDetail() {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value)
Boolean(value),
);
}}
/>
@ -1509,251 +1551,262 @@ export default function FormAudioDetail() {
</div>
{/* Detail Wilayah */}
{fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* {fileUnitSelections[index]?.wilayah && ( */}
{!isContentFromSatker &&
fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
);
setupPlacement(
index,
item.key,
Boolean(value)
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
{item.label}
</Label>
</div>
))}
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value),
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id)
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id
);
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{item.label}
</Label>
</div>
))}
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[polda.id] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
)
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
polda
)
}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={12}
height={
12
}
className="mr-1"
/>
Batal Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={12}
height={
12
}
className="mr-1"
/>
Pilih Semua
</>
)}
</Button>
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan
POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id),
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id,
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[
polda.id
] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
sub.id,
),
),
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
Number(
sub.id
)
polda,
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
)
)}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={
12
}
height={
12
}
className="mr-1"
/>
Batal
Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={
12
}
height={
12
}
className="mr-1"
/>
Pilih
Semua
</>
)}
</Button>
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(
sub.id,
),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(
sub.id,
),
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
),
)}
</div>
</div>
</div>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
defaultValue: "Simpan",
})} */}
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
)}
)}
</div>
</div>
) : (
@ -1832,7 +1885,7 @@ export default function FormAudioDetail() {
>
{template}
</Button>
)
),
)}
</div>
</div>

View File

@ -1806,7 +1806,7 @@ export default function FormAudio() {
</Card>
<div className="flex flex-row justify-end gap-3">
<div className="mt-4">
{levelNumber !== "2" && levelNumber !== "3" && (
{levelNumber !== "2" && (
<Button type="submit" color="primary">
{t("submit", { defaultValue: "Submit" })}
</Button>

View File

@ -115,7 +115,7 @@ const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
{ ssr: false },
);
interface Destination {
@ -172,6 +172,12 @@ export default function FormImageDetail() {
satker: boolean;
}>
>([]);
const [creatorLevelNumber, setCreatorLevelNumber] = useState<number | null>(
null,
);
const isContentFromSatker = creatorLevelNumber === 3;
const isContentFromMabesOrPolda =
creatorLevelNumber === 1 || creatorLevelNumber === 2;
useEffect(() => {
if (Number(userLevelId) === 216 && Number(roleId) === 3) {
@ -200,7 +206,7 @@ export default function FormImageDetail() {
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [expandedPolda, setExpandedPolda] = useState<Record<number, boolean>>(
{}
{},
);
// State untuk melacak apakah perubahan berasal dari checkbox utama
@ -231,7 +237,7 @@ export default function FormImageDetail() {
useEffect(() => {
if (detail?.assignedToTopLevel) {
const outputSet = new Set(
detail.assignedToTopLevel.split(",").map(Number)
detail.assignedToTopLevel.split(",").map(Number),
);
setUnitSelection({
semua: outputSet.has(0),
@ -260,7 +266,7 @@ export default function FormImageDetail() {
acc[polda.id] = false;
return acc;
},
{}
{},
);
setExpandedPolda(initialExpandedState);
console.log("polres", initialExpandedState);
@ -299,7 +305,7 @@ export default function FormImageDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
checkedLevels.has(Number(item.id)),
).length;
const checkedPolresCount = listDest.reduce((total: number, item: any) => {
@ -308,7 +314,7 @@ export default function FormImageDetail() {
return (
total +
item.subDestination.filter((sub: any) =>
checkedLevels.has(Number(sub.id))
checkedLevels.has(Number(sub.id)),
).length
);
}
@ -316,12 +322,12 @@ export default function FormImageDetail() {
}, 0);
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
const checkedSatkerCount = satkerItem
? (checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) +
(satkerItem.subDestination?.filter((sub: any) =>
checkedLevels.has(Number(sub.id))
checkedLevels.has(Number(sub.id)),
).length || 0)
: 0;
@ -391,7 +397,7 @@ export default function FormImageDetail() {
} else if (mainCheckboxChangeType === "satker_checked") {
// Checklist satker
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
newCheckedLevels.add(Number(satkerItem.id));
@ -439,7 +445,7 @@ export default function FormImageDetail() {
} else if (mainCheckboxChangeType === "satker_unchecked") {
// Clear satker dan semua sub-item di bawahnya dari checkedLevels
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
newCheckedLevels.delete(Number(satkerItem.id));
@ -466,7 +472,7 @@ export default function FormImageDetail() {
const handleFileUnitChange = (
fileIndex: number,
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
setFileUnitSelections((prev) => {
const newSelections = [...prev];
@ -486,7 +492,7 @@ export default function FormImageDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[fileIndex] || new Set()
newArray[fileIndex] || new Set(),
);
if (value) {
@ -517,12 +523,12 @@ export default function FormImageDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
currentFileCheckedLevels.has(Number(item.id))
currentFileCheckedLevels.has(Number(item.id)),
);
if (!hasSelectedPolda) {
alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES.",
);
return prev; // Batalkan perubahan
}
@ -552,7 +558,7 @@ export default function FormImageDetail() {
// Fungsi lama untuk kompatibilitas (akan dihapus nanti)
const handleUnitChange = (
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
// Set flag bahwa perubahan berasal dari checkbox utama
setIsUpdatingFromMainCheckbox(true);
@ -578,13 +584,13 @@ export default function FormImageDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
checkedLevels.has(Number(item.id)),
);
if (!hasSelectedPolda) {
// Jika tidak ada POLDA yang dipilih, tampilkan peringatan dan batalkan
alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES.",
);
// Reset flag karena perubahan dibatalkan
setIsUpdatingFromMainCheckbox(false);
@ -640,14 +646,14 @@ 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],
);
};
// Fungsi untuk mengupdate checklist levels untuk file tertentu
const handleFileCheckboxChangePlacement = (
fileIndex: number,
levelId: number
levelId: number,
) => {
setFileCheckedLevels((prev) => {
const newArray = [...prev];
@ -659,7 +665,7 @@ export default function FormImageDetail() {
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (
poldaItem &&
@ -682,7 +688,7 @@ export default function FormImageDetail() {
// Jika ini adalah SATKER POLRI yang di-checklist, checklist juga semua sub-item di bawahnya
const satkerItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (satkerItem && satkerItem.name === "SATKER POLRI") {
// Checklist semua sub-item di bawah SATKER POLRI (bukan POLRES)
@ -710,7 +716,7 @@ export default function FormImageDetail() {
// Cek apakah semua sub-items sudah ter-checklist
const allSubItemsChecked = polda.subDestination?.every((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
);
if (allSubItemsChecked) {
@ -750,7 +756,7 @@ export default function FormImageDetail() {
// Hitung total POLDA yang ada (bukan SATKER POLRI)
const totalPoldaCount = listDest.filter(
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI"
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI",
).length;
// Hitung berapa banyak POLDA yang ter-checklist (bukan SATKER POLRI)
@ -777,7 +783,7 @@ export default function FormImageDetail() {
}
return total;
},
0
0,
);
// Hitung berapa banyak POLRES yang ter-checklist
@ -787,7 +793,7 @@ export default function FormImageDetail() {
return (
total +
item.subDestination.filter((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
).length
);
}
@ -796,7 +802,7 @@ export default function FormImageDetail() {
// Cek apakah SATKER POLRI ter-checklist
const satkerItem = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
const isSatkerChecked =
satkerItem && currentFileLevels.has(Number(satkerItem.id));
@ -833,7 +839,7 @@ export default function FormImageDetail() {
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (
poldaItem &&
@ -856,7 +862,7 @@ export default function FormImageDetail() {
// Jika ini adalah SATKER POLRI yang di-checklist, checklist juga semua sub-item di bawahnya
const satkerItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (satkerItem && satkerItem.name === "SATKER POLRI") {
// Checklist semua sub-item di bawah SATKER POLRI (bukan POLRES)
@ -887,7 +893,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) {
@ -936,6 +942,11 @@ export default function FormImageDetail() {
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
if (details?.uploadedBy?.userLevel?.levelNumber) {
setCreatorLevelNumber(details.uploadedBy.userLevel.levelNumber);
}
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
@ -946,14 +957,14 @@ export default function FormImageDetail() {
if (details?.assignedToLevel) {
const levels = new Set<number>(
details.assignedToLevel.split(",").map(Number)
details.assignedToLevel.split(",").map(Number),
);
setCheckedLevels(levels);
}
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject?.map(
(obj: any) => obj.id
(obj: any) => obj.id,
);
setSelectedPublishers(publisherIds);
}
@ -963,7 +974,7 @@ export default function FormImageDetail() {
const filesData = details.files || [];
const fileUrls = filesData.map((file: { thumbnailFileUrl: string }) =>
file.thumbnailFileUrl ? file.thumbnailFileUrl : "default-image.jpg"
file.thumbnailFileUrl ? file.thumbnailFileUrl : "default-image.jpg",
);
setDetailThumb(fileUrls);
@ -1083,7 +1094,7 @@ export default function FormImageDetail() {
const setupPlacement = (
index: number,
placement: string,
checked: boolean
checked: boolean,
) => {
let temp = [...filePlacements];
if (checked) {
@ -1094,7 +1105,7 @@ export default function FormImageDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Checklist semua item di modal
@ -1144,7 +1155,7 @@ export default function FormImageDetail() {
// Hanya auto-checklist "all" jika polda, polres, dan mabes ter-checklist
// JANGAN include satker dalam perhitungan auto "all"
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length === 3 && !now.includes("all")) {
now.push("all");
@ -1159,7 +1170,7 @@ export default function FormImageDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Unchecklist semua item di modal
@ -1193,7 +1204,7 @@ export default function FormImageDetail() {
// Hapus "all" jika tidak semua item ter-checklist
if (now.includes("all")) {
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length < 3) {
const newData = now.filter((b) => b !== "all");
@ -1212,7 +1223,7 @@ export default function FormImageDetail() {
const updateModalChecklistLevels = (
fileIndex: number,
placement: string,
checked: boolean
checked: boolean,
) => {
if (!listDest || listDest.length === 0) return;
@ -1245,7 +1256,7 @@ export default function FormImageDetail() {
} else if (placement === "satker") {
// Checklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.add(Number(satkerItem.id));
@ -1281,7 +1292,7 @@ export default function FormImageDetail() {
} else if (placement === "satker") {
// Unchecklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.delete(Number(satkerItem.id));
@ -1311,7 +1322,7 @@ export default function FormImageDetail() {
type: string,
url: string,
names: string,
format: string
format: string,
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
@ -1411,7 +1422,7 @@ export default function FormImageDetail() {
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
String(cat.id) === String(detail.category.id),
) && (
<SelectItem
key={String(detail.category.id)}
@ -1732,7 +1743,7 @@ export default function FormImageDetail() {
Tingkat Distribusi:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{/* {[
{ key: "semua", label: "Semua" },
{ key: "nasional", label: "Nasional" },
{ key: "wilayah", label: "Wilayah" },
@ -1740,6 +1751,39 @@ export default function FormImageDetail() {
key: "international",
label: "Internasional",
},
] */}
{[
{ key: "semua", label: "Semua" },
...(isContentFromMabesOrPolda
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
{
key: "wilayah",
label: "Wilayah",
},
]
: []),
...(isContentFromSatker
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
]
: []),
].map((item, idx) => (
<div
key={item.key}
@ -1756,12 +1800,12 @@ export default function FormImageDetail() {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value)
Boolean(value),
);
}}
/>
@ -1777,246 +1821,257 @@ export default function FormImageDetail() {
</div>
{/* Detail Wilayah */}
{fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* {fileUnitSelections[index]?.wilayah && ( */}
{!isContentFromSatker &&
fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
);
setupPlacement(
index,
item.key,
Boolean(value)
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
{item.label}
</Label>
</div>
))}
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value),
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id)
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id
);
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{item.label}
</Label>
</div>
))}
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[polda.id] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
)
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
polda
)
}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={12}
height={
12
}
className="mr-1"
/>
Batal Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={12}
height={
12
}
className="mr-1"
/>
Pilih Semua
</>
)}
</Button>
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan
POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id),
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id,
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[
polda.id
] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
sub.id,
),
),
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
Number(
sub.id
)
polda,
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
)
)}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={
12
}
height={
12
}
className="mr-1"
/>
Batal
Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={
12
}
height={
12
}
className="mr-1"
/>
Pilih
Semua
</>
)}
</Button>
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(
sub.id,
),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(
sub.id,
),
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
),
)}
</div>
</div>
</div>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>Simpan</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>Simpan</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
)}
)}
</div>
</div>
) : (
@ -2095,7 +2150,7 @@ export default function FormImageDetail() {
>
{template}
</Button>
)
),
)}
</div>
</div>

View File

@ -82,7 +82,7 @@ const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
{ ssr: false },
);
export default function FormImage() {
@ -117,7 +117,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] =
@ -183,17 +183,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;
}
@ -227,12 +227,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
@ -454,7 +454,7 @@ export default function FormImage() {
const articleData = await waitForStatusUpdate();
const cleanArticleBody = articleData?.articleBody?.replace(
/<img[^>]*>/g,
""
"",
);
const articleImagesData = articleData?.imagesUrl?.split(",");
setArticleBody(cleanArticleBody || "");
@ -507,7 +507,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) {
@ -530,7 +530,7 @@ export default function FormImage() {
setPublishedFor(
options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id)
.map((opt: any) => opt.id),
);
}
} else {
@ -594,11 +594,16 @@ export default function FormImage() {
} else if (data.rewriteDescription && selectedFileType === "rewrite") {
finalDescription = data.rewriteDescription;
console.log("📤 Upload versi rewrite");
} else {
// fallback: gunakan versi Indonesia original
finalDescription = data.descriptionOri ?? "";
console.log("📤 Upload versi Indonesia (original)");
} else if (data.description?.trim()) {
finalDescription = data.description;
} else if (data.descriptionOri?.trim()) {
finalDescription = data.descriptionOri;
}
// else {
// // fallback: gunakan versi Indonesia original
// finalDescription = data.descriptionOri ?? "";
// console.log("📤 Upload versi Indonesia (original)");
// }
if (!finalDescription?.trim()) {
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
@ -690,7 +695,7 @@ export default function FormImage() {
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0",
);
});
@ -717,7 +722,7 @@ export default function FormImage() {
idx: number,
id: string,
file: any,
duration: string
duration: string,
) {
console.log(idx, id, file, duration);
@ -753,7 +758,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;

View File

@ -114,7 +114,7 @@ const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
{ ssr: false },
);
interface Destination {
@ -191,6 +191,12 @@ export default function FormTeksDetail() {
satker: boolean;
}>
>([]);
const [creatorLevelNumber, setCreatorLevelNumber] = useState<number | null>(
null,
);
const isContentFromSatker = creatorLevelNumber === 3;
const isContentFromMabesOrPolda =
creatorLevelNumber === 1 || creatorLevelNumber === 2;
useEffect(() => {
if (Number(userLevelId) === 216 && Number(roleId) === 3) {
@ -202,7 +208,7 @@ export default function FormTeksDetail() {
const handleFileUnitChange = (
fileIndex: number,
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
setFileUnitSelections((prev) => {
const newSelections = [...prev];
@ -222,7 +228,7 @@ export default function FormTeksDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[fileIndex] || new Set()
newArray[fileIndex] || new Set(),
);
if (value) {
@ -253,12 +259,12 @@ export default function FormTeksDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
currentFileCheckedLevels.has(Number(item.id))
currentFileCheckedLevels.has(Number(item.id)),
);
if (!hasSelectedPolda) {
alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES.",
);
return prev; // Batalkan perubahan
}
@ -297,7 +303,7 @@ export default function FormTeksDetail() {
useEffect(() => {
if (detail?.assignedToTopLevel) {
const outputSet = new Set(
detail.assignedToTopLevel.split(",").map(Number)
detail.assignedToTopLevel.split(",").map(Number),
);
setUnitSelection({
semua: outputSet.has(0),
@ -322,7 +328,7 @@ export default function FormTeksDetail() {
acc[polda.id] = false;
return acc;
},
{}
{},
);
setExpandedPolda(initialExpandedState);
console.log("polres", initialExpandedState);
@ -357,7 +363,7 @@ export default function FormTeksDetail() {
const handleUnitChange = (
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
if (key === "semua") {
const newState = {
@ -436,7 +442,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],
);
};
@ -458,7 +464,7 @@ export default function FormTeksDetail() {
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
o.name.toLowerCase().includes("pers rilis"),
);
if (findCategory) {
@ -507,6 +513,9 @@ export default function FormTeksDetail() {
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
if (details?.uploadedBy?.userLevel?.levelNumber) {
setCreatorLevelNumber(details.uploadedBy.userLevel.levelNumber);
}
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
@ -517,14 +526,14 @@ export default function FormTeksDetail() {
if (details?.assignedToLevel) {
const levels = new Set(
details.assignedToLevel.split(",").map(Number)
details.assignedToLevel.split(",").map(Number),
);
setCheckedLevels(levels);
}
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject?.map(
(obj: any) => obj.id
(obj: any) => obj.id,
);
setSelectedPublishers(publisherIds);
}
@ -696,7 +705,7 @@ export default function FormTeksDetail() {
type: string,
url: string,
names: string,
format: string
format: string,
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
@ -723,7 +732,7 @@ export default function FormTeksDetail() {
const setupPlacement = (
index: number,
placement: string,
checked: boolean
checked: boolean,
) => {
let temp = [...filePlacements];
if (checked) {
@ -734,7 +743,7 @@ export default function FormTeksDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Checklist semua item di modal
@ -784,7 +793,7 @@ export default function FormTeksDetail() {
// Hanya auto-checklist "all" jika polda, polres, dan mabes ter-checklist
// JANGAN include satker dalam perhitungan auto "all"
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length === 3 && !now.includes("all")) {
now.push("all");
@ -799,7 +808,7 @@ export default function FormTeksDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Unchecklist semua item di modal
@ -833,7 +842,7 @@ export default function FormTeksDetail() {
// Hapus "all" jika tidak semua item ter-checklist
if (now.includes("all")) {
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length < 3) {
const newData = now.filter((b) => b !== "all");
@ -851,7 +860,7 @@ export default function FormTeksDetail() {
const updateModalChecklistLevels = (
fileIndex: number,
placement: string,
checked: boolean
checked: boolean,
) => {
if (!listDest || listDest.length === 0) return;
@ -884,7 +893,7 @@ export default function FormTeksDetail() {
} else if (placement === "satker") {
// Checklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.add(Number(satkerItem.id));
@ -920,7 +929,7 @@ export default function FormTeksDetail() {
} else if (placement === "satker") {
// Unchecklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.delete(Number(satkerItem.id));
@ -953,7 +962,7 @@ export default function FormTeksDetail() {
// Fungsi untuk mengupdate checklist levels untuk file tertentu
const handleFileCheckboxChangePlacement = (
fileIndex: number,
levelId: number
levelId: number,
) => {
setFileCheckedLevels((prev) => {
const newArray = [...prev];
@ -965,7 +974,7 @@ export default function FormTeksDetail() {
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (
poldaItem &&
@ -988,7 +997,7 @@ export default function FormTeksDetail() {
// Jika ini adalah SATKER POLRI yang di-checklist, checklist juga semua sub-item di bawahnya
const satkerItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (satkerItem && satkerItem.name === "SATKER POLRI") {
// Checklist semua sub-item di bawah SATKER POLRI (bukan POLRES)
@ -1019,7 +1028,7 @@ export default function FormTeksDetail() {
// Hitung total POLDA yang ada (bukan SATKER POLRI)
const totalPoldaCount = listDest.filter(
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI"
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI",
).length;
// Hitung berapa banyak POLDA yang ter-checklist (bukan SATKER POLRI)
@ -1046,7 +1055,7 @@ export default function FormTeksDetail() {
}
return total;
},
0
0,
);
// Hitung berapa banyak POLRES yang ter-checklist
@ -1056,7 +1065,7 @@ export default function FormTeksDetail() {
return (
total +
item.subDestination.filter((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
).length
);
}
@ -1065,7 +1074,7 @@ export default function FormTeksDetail() {
// Cek apakah SATKER POLRI ter-checklist
const satkerItem = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
const isSatkerChecked =
satkerItem && currentFileLevels.has(Number(satkerItem.id));
@ -1099,7 +1108,7 @@ export default function FormTeksDetail() {
// Cek apakah semua sub-items sudah ter-checklist
const allSubItemsChecked = polda.subDestination?.every((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
);
if (allSubItemsChecked) {
@ -1182,7 +1191,7 @@ export default function FormTeksDetail() {
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
String(cat.id) === String(detail.category.id),
) && (
<SelectItem
key={String(detail.category.id)}
@ -1465,7 +1474,7 @@ export default function FormTeksDetail() {
Tingkat Distribusi:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{/* {[
{ key: "semua", label: "Semua" },
{ key: "nasional", label: "Nasional" },
{ key: "wilayah", label: "Wilayah" },
@ -1473,6 +1482,39 @@ export default function FormTeksDetail() {
key: "international",
label: "Internasional",
},
] */}
{[
{ key: "semua", label: "Semua" },
...(isContentFromMabesOrPolda
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
{
key: "wilayah",
label: "Wilayah",
},
]
: []),
...(isContentFromSatker
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
]
: []),
].map((item, idx) => (
<div
key={item.key}
@ -1489,12 +1531,12 @@ export default function FormTeksDetail() {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value)
Boolean(value),
);
}}
/>
@ -1510,251 +1552,262 @@ export default function FormTeksDetail() {
</div>
{/* Detail Wilayah */}
{fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* {fileUnitSelections[index]?.wilayah && ( */}
{!isContentFromSatker &&
fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
);
setupPlacement(
index,
item.key,
Boolean(value)
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
{item.label}
</Label>
</div>
))}
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value),
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id)
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id
);
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{item.label}
</Label>
</div>
))}
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[polda.id] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
)
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
polda
)
}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={12}
height={
12
}
className="mr-1"
/>
Batal Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={12}
height={
12
}
className="mr-1"
/>
Pilih Semua
</>
)}
</Button>
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan
POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id),
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id,
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[
polda.id
] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
sub.id,
),
),
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
Number(
sub.id
)
polda,
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
)
)}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={
12
}
height={
12
}
className="mr-1"
/>
Batal
Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={
12
}
height={
12
}
className="mr-1"
/>
Pilih
Semua
</>
)}
</Button>
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(
sub.id,
),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(
sub.id,
),
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
),
)}
</div>
</div>
</div>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
defaultValue: "Simpan",
})} */}
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
)}
)}
</div>
</div>
) : (
@ -1833,7 +1886,7 @@ export default function FormTeksDetail() {
>
{template}
</Button>
)
),
)}
</div>
</div>

View File

@ -1776,7 +1776,7 @@ export default function FormTeks() {
{/* <Button type="submit" color="primary">
{t("submit", { defaultValue: "Submit" })}
</Button> */}
{levelNumber !== "2" && levelNumber !== "3" && (
{levelNumber !== "2" && (
<Button type="submit" color="primary">
{t("submit", { defaultValue: "Submit" })}
</Button>

View File

@ -115,7 +115,7 @@ const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
{ ssr: false },
);
interface Destination {
@ -190,6 +190,12 @@ export default function FormVideoDetail() {
satker: boolean;
}>
>([]);
const [creatorLevelNumber, setCreatorLevelNumber] = useState<number | null>(
null,
);
const isContentFromSatker = creatorLevelNumber === 3;
const isContentFromMabesOrPolda =
creatorLevelNumber === 1 || creatorLevelNumber === 2;
useEffect(() => {
if (Number(userLevelId) === 216 && Number(roleId) === 3) {
@ -201,7 +207,7 @@ export default function FormVideoDetail() {
const handleFileUnitChange = (
fileIndex: number,
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
setFileUnitSelections((prev) => {
const newSelections = [...prev];
@ -221,7 +227,7 @@ export default function FormVideoDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[fileIndex] || new Set()
newArray[fileIndex] || new Set(),
);
if (value) {
@ -252,12 +258,12 @@ export default function FormVideoDetail() {
(item: any) =>
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
currentFileCheckedLevels.has(Number(item.id))
currentFileCheckedLevels.has(Number(item.id)),
);
if (!hasSelectedPolda) {
alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES.",
);
return prev; // Batalkan perubahan
}
@ -296,7 +302,7 @@ export default function FormVideoDetail() {
useEffect(() => {
if (detail?.assignedToTopLevel) {
const outputSet = new Set(
detail.assignedToTopLevel.split(",").map(Number)
detail.assignedToTopLevel.split(",").map(Number),
);
setUnitSelection({
semua: outputSet.has(0),
@ -321,7 +327,7 @@ export default function FormVideoDetail() {
acc[polda.id] = false;
return acc;
},
{}
{},
);
setExpandedPolda(initialExpandedState);
console.log("polres", initialExpandedState);
@ -356,7 +362,7 @@ export default function FormVideoDetail() {
const handleUnitChange = (
key: keyof typeof unitSelection,
value: boolean
value: boolean,
) => {
if (key === "semua") {
const newState = {
@ -435,7 +441,7 @@ export default function FormVideoDetail() {
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],
);
};
@ -457,7 +463,7 @@ export default function FormVideoDetail() {
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
o.name.toLowerCase().includes("pers rilis"),
);
if (findCategory) {
@ -480,6 +486,10 @@ export default function FormVideoDetail() {
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
if (details?.uploadedBy?.userLevel?.levelNumber) {
setCreatorLevelNumber(details.uploadedBy.userLevel.levelNumber);
}
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
@ -489,14 +499,14 @@ export default function FormVideoDetail() {
if (details?.assignedToLevel) {
const levels = new Set(
details.assignedToLevel.split(",").map(Number)
details.assignedToLevel.split(",").map(Number),
);
setCheckedLevels(levels);
}
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject?.map(
(obj: any) => obj.id
(obj: any) => obj.id,
);
setSelectedPublishers(publisherIds);
}
@ -506,7 +516,7 @@ export default function FormVideoDetail() {
const filesData = details?.files || [];
const fileUrls = filesData.map((files: { url: string }) =>
files.url ? files.url : "default-image.jpg"
files.url ? files.url : "default-image.jpg",
);
setDetailVideo(fileUrls);
@ -684,7 +694,7 @@ export default function FormVideoDetail() {
const setupPlacement = (
index: number,
placement: string,
checked: boolean
checked: boolean,
) => {
let temp = [...filePlacements];
if (checked) {
@ -695,7 +705,7 @@ export default function FormVideoDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Checklist semua item di modal
@ -745,7 +755,7 @@ export default function FormVideoDetail() {
// Hanya auto-checklist "all" jika polda, polres, dan mabes ter-checklist
// JANGAN include satker dalam perhitungan auto "all"
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length === 3 && !now.includes("all")) {
now.push("all");
@ -760,7 +770,7 @@ export default function FormVideoDetail() {
setFileCheckedLevels((prevLevels) => {
const newArray = [...prevLevels];
const currentFileLevels = new Set<number>(
newArray[index] || new Set()
newArray[index] || new Set(),
);
// Unchecklist semua item di modal
@ -794,7 +804,7 @@ export default function FormVideoDetail() {
// Hapus "all" jika tidak semua item ter-checklist
if (now.includes("all")) {
const nonSatkerItems = now.filter(
(item) => item !== "satker" && item !== "all"
(item) => item !== "satker" && item !== "all",
);
if (nonSatkerItems.length < 3) {
const newData = now.filter((b) => b !== "all");
@ -812,7 +822,7 @@ export default function FormVideoDetail() {
const updateModalChecklistLevels = (
fileIndex: number,
placement: string,
checked: boolean
checked: boolean,
) => {
if (!listDest || listDest.length === 0) return;
@ -845,7 +855,7 @@ export default function FormVideoDetail() {
} else if (placement === "satker") {
// Checklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.add(Number(satkerItem.id));
@ -881,7 +891,7 @@ export default function FormVideoDetail() {
} else if (placement === "satker") {
// Unchecklist SATKER POLRI dan semua sub-item di bawahnya
const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
if (satkerItem) {
currentFileLevels.delete(Number(satkerItem.id));
@ -910,7 +920,7 @@ export default function FormVideoDetail() {
type: string,
url: string,
names: string,
format: string
format: string,
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
@ -951,7 +961,7 @@ export default function FormVideoDetail() {
// Fungsi untuk mengupdate checklist levels untuk file tertentu
const handleFileCheckboxChangePlacement = (
fileIndex: number,
levelId: number
levelId: number,
) => {
setFileCheckedLevels((prev) => {
const newArray = [...prev];
@ -963,7 +973,7 @@ export default function FormVideoDetail() {
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (
poldaItem &&
@ -986,7 +996,7 @@ export default function FormVideoDetail() {
// Jika ini adalah SATKER POLRI yang di-checklist, checklist juga semua sub-item di bawahnya
const satkerItem = listDest.find(
(item: any) => Number(item.id) === levelId
(item: any) => Number(item.id) === levelId,
) as any;
if (satkerItem && satkerItem.name === "SATKER POLRI") {
// Checklist semua sub-item di bawah SATKER POLRI (bukan POLRES)
@ -1017,7 +1027,7 @@ export default function FormVideoDetail() {
// Hitung total POLDA yang ada (bukan SATKER POLRI)
const totalPoldaCount = listDest.filter(
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI"
(item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI",
).length;
// Hitung berapa banyak POLDA yang ter-checklist (bukan SATKER POLRI)
@ -1044,7 +1054,7 @@ export default function FormVideoDetail() {
}
return total;
},
0
0,
);
// Hitung berapa banyak POLRES yang ter-checklist
@ -1054,7 +1064,7 @@ export default function FormVideoDetail() {
return (
total +
item.subDestination.filter((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
).length
);
}
@ -1063,7 +1073,7 @@ export default function FormVideoDetail() {
// Cek apakah SATKER POLRI ter-checklist
const satkerItem = listDest.find(
(item: any) => item.name === "SATKER POLRI"
(item: any) => item.name === "SATKER POLRI",
);
const isSatkerChecked =
satkerItem && currentFileLevels.has(Number(satkerItem.id));
@ -1097,7 +1107,7 @@ export default function FormVideoDetail() {
// Cek apakah semua sub-items sudah ter-checklist
const allSubItemsChecked = polda.subDestination?.every((sub: any) =>
currentFileLevels.has(Number(sub.id))
currentFileLevels.has(Number(sub.id)),
);
if (allSubItemsChecked) {
@ -1180,7 +1190,7 @@ export default function FormVideoDetail() {
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
String(cat.id) === String(detail.category.id),
) && (
<SelectItem
key={String(detail.category.id)}
@ -1465,7 +1475,7 @@ export default function FormVideoDetail() {
Tingkat Distribusi:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{/* {[
{ key: "semua", label: "Semua" },
{ key: "nasional", label: "Nasional" },
{ key: "wilayah", label: "Wilayah" },
@ -1473,6 +1483,39 @@ export default function FormVideoDetail() {
key: "international",
label: "Internasional",
},
] */}
{[
{ key: "semua", label: "Semua" },
...(isContentFromMabesOrPolda
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
{
key: "wilayah",
label: "Wilayah",
},
]
: []),
...(isContentFromSatker
? [
{
key: "nasional",
label: "Nasional",
},
{
key: "international",
label: "Internasional",
},
]
: []),
].map((item, idx) => (
<div
key={item.key}
@ -1489,12 +1532,12 @@ export default function FormVideoDetail() {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value)
Boolean(value),
);
}}
/>
@ -1510,251 +1553,262 @@ export default function FormVideoDetail() {
</div>
{/* Detail Wilayah */}
{fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* {fileUnitSelections[index]?.wilayah && ( */}
{!isContentFromSatker &&
fileUnitSelections[index]?.wilayah && (
<div className="border-t border-gray-200 pt-2">
<p className="text-sm font-medium text-gray-700 mb-2">
Detail Wilayah:
</p>
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean
);
setupPlacement(
index,
item.key,
Boolean(value)
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
{/* Checkbox Sub-kategori dengan tombol Kustom sejajar */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
{[
{ key: "polda", label: "POLDA" },
{ key: "polres", label: "POLRES" },
{ key: "satker", label: "SATKER" },
].map((item, idx) => (
<div
key={item.key}
className="flex items-center gap-2 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
{item.label}
</Label>
</div>
))}
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
<Checkbox
id={`${item.key}-${index}`}
checked={
fileUnitSelections[index]?.[
item.key as keyof typeof unitSelection
] || false
}
onCheckedChange={(value) => {
handleFileUnitChange(
index,
item.key as keyof typeof unitSelection,
value as boolean,
);
setupPlacement(
index,
item.key,
Boolean(value),
);
}}
/>
<Label
htmlFor={`${item.key}-${index}`}
className="text-sm font-medium cursor-pointer"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id)
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id
);
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{item.label}
</Label>
</div>
))}
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[polda.id] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
)
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
polda
)
}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={12}
height={
12
}
className="mr-1"
/>
Batal Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={12}
height={
12
}
className="mr-1"
/>
Pilih Semua
</>
)}
</Button>
{/* Tombol Kustom sejajar dengan checkbox */}
<div className="flex items-center justify-center p-3">
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Icon
icon="material-symbols:tune"
width={16}
height={16}
/>
{t("custom", {
defaultValue: "Kustom",
})}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[95vw] lg:max-w-[1400px] max-h-[90vh]">
<DialogHeader className="border-b border-gray-200 pb-4">
<DialogTitle className="text-lg font-semibold">
Daftar Wilayah POLDA dan
POLRES
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 max-h-[70vh] overflow-y-auto p-1">
{listDest.map((polda: any) => (
<div
key={polda.id}
className="border border-gray-200 rounded-lg p-2 bg-white hover:shadow-sm transition-shadow"
>
{/* Header POLDA */}
<div className="flex items-center justify-between">
<Label className="flex items-center gap-3 flex-1 cursor-pointer">
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(polda.id),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(polda.id),
)
}
/>
<span className="font-semibold text-gray-900 text-sm">
{polda.name}
</span>
</Label>
{polda.subDestination && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleExpand(
polda.id,
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
}}
className="p-1 hover:bg-gray-100 rounded-md transition-colors"
>
<Icon
icon={
expandedPolda[
polda.id
]
? "mdi:chevron-up"
: "mdi:chevron-down"
}
width={16}
height={16}
/>
</button>
)}
</div>
{/* Sub-items */}
{polda.subDestination &&
expandedPolda[
polda.id
] && (
<div className="max-h-[200px] overflow-y-auto border-t border-gray-100 pt-2">
{/* Tombol Pilih Semua untuk sub-items */}
<div className="mb-2 flex justify-start">
{(() => {
const allSubItemsChecked =
polda.subDestination?.every(
(sub: any) =>
fileCheckedLevels[
index
]?.has(
Number(
sub.id
)
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
sub.id,
),
),
);
return (
<Button
size="sm"
variant="outline"
className="text-xs h-6 px-2"
onClick={() =>
handleSelectAllSubItems(
index,
Number(
sub.id
)
polda,
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
)
)}
>
{allSubItemsChecked ? (
<>
<Icon
icon="material-symbols:check-indeterminate-small"
width={
12
}
height={
12
}
className="mr-1"
/>
Batal
Semua
</>
) : (
<>
<Icon
icon="material-symbols:check-all"
width={
12
}
height={
12
}
className="mr-1"
/>
Pilih
Semua
</>
)}
</Button>
);
})()}
</div>
<div className="space-y-1">
{polda.subDestination.map(
(sub: any) => (
<Label
key={sub.id}
className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50 transition-colors cursor-pointer text-xs"
>
<Checkbox
checked={
fileCheckedLevels[
index
]?.has(
Number(
sub.id,
),
) || false
}
onCheckedChange={() =>
handleFileCheckboxChangePlacement(
index,
Number(
sub.id,
),
)
}
/>
<span className="text-gray-700">
{sub.name}
</span>
</Label>
),
)}
</div>
</div>
</div>
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
)}
</div>
))}
</div>
<div className="flex justify-end gap-3 border-t border-gray-200 pt-4">
<DialogClose asChild>
<Button variant="outline">
{t("cancel", {
defaultValue: "Batal",
})}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>
{/* {t("save", {
defaultValue: "Simpan",
})} */}
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
Simpan
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
)}
)}
</div>
</div>
) : (
@ -1833,7 +1887,7 @@ export default function FormVideoDetail() {
>
{template}
</Button>
)
),
)}
</div>
</div>

View File

@ -59,7 +59,7 @@ const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
{ ssr: false },
);
interface FileWithPreview extends File {
@ -111,11 +111,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 [selectedSize, setSelectedSize] = useState("");
const [detailData, setDetailData] = useState<any>(null);
@ -184,7 +184,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) => {
@ -211,11 +211,11 @@ export default function FormVideo() {
.refine(
(files) =>
files.every((file: File) => ACCEPTED_FILE_TYPES.includes(file.type)),
{ message: "File harus berformat mp4 atau mov" }
{ message: "File harus berformat mp4 atau mov" },
)
.refine(
(files) => files.every((file: File) => file.size <= MAX_FILE_SIZE),
{ message: "Ukuran file maksimal 100 MB" }
{ message: "Ukuran file maksimal 100 MB" },
),
publishedFor: z
.array(z.string())
@ -423,7 +423,7 @@ export default function FormVideo() {
const articleData = await waitForStatusUpdate();
const cleanArticleBody = articleData?.articleBody?.replace(
/<img[^>]*>/g,
""
"",
);
const articleImagesData = articleData?.imagesUrl?.split(",");
setValue("description", cleanArticleBody || "");
@ -479,7 +479,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) {
@ -502,7 +502,7 @@ export default function FormVideo() {
setPublishedFor(
options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id)
.map((opt: any) => opt.id),
);
}
} else {
@ -561,8 +561,8 @@ export default function FormVideo() {
translatedTitle && translatedTitle.trim() !== ""
? translatedTitle
: isSwitchOn
? title
: data.title;
? title
: data.title;
// Tentukan deskripsi final:
// Jika ada hasil translate, kirim itu ke backend
@ -570,10 +570,10 @@ export default function FormVideo() {
translatedContent && translatedContent.trim() !== ""
? translatedContent
: isSwitchOn
? data.description
: selectedFileType === "rewrite"
? data.rewriteDescription
: data.descriptionOri;
? data.description
: selectedFileType === "rewrite"
? data.rewriteDescription
: data.descriptionOri;
if (!finalDescription?.trim()) {
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
@ -695,7 +695,7 @@ export default function FormVideo() {
idx: number,
id: string,
file: any,
duration: string
duration: string,
) {
console.log(idx, id, file, duration);
@ -731,7 +731,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;
@ -791,8 +791,8 @@ export default function FormVideo() {
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setThumbnail(file);
setPreview(URL.createObjectURL(file));
setThumbnail(file);
setPreview(URL.createObjectURL(file));
console.log("Selected Thumbnail:", file);
}
};
@ -823,7 +823,7 @@ export default function FormVideo() {
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
>
<div className="flex gap-3 items-center">
{/* <div className="file-preview">{renderFilePreview(file)}</div> */}
<svg
@ -1732,7 +1732,7 @@ export default function FormVideo() {
type="button"
onClick={() => {
const updatedTags = field.value.filter(
(_, i) => i !== index
(_, i) => i !== index,
);
field.onChange(updatedTags);
}}
@ -1825,7 +1825,7 @@ export default function FormVideo() {
{/* <Button type="submit" color="primary">
{t("submit", { defaultValue: "Submit" })}
</Button> */}
{levelNumber !== "2" && levelNumber !== "3" && (
{levelNumber !== "2" && (
<Button type="submit" color="primary">
{t("submit", { defaultValue: "Submit" })}
</Button>

View File

@ -75,6 +75,7 @@ interface Detail {
youtubeUrl: string;
needApprovalFrom: number;
uploadedById: number;
statusId?: number;
}
export default function FormDetailLiveReport() {
@ -101,6 +102,11 @@ export default function FormDetailLiveReport() {
const [status, setStatus] = useState("");
const [description, setDescription] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [uploaderLevelNumber, setUploaderLevelNumber] = useState<number | null>(
null,
);
const isScheduleFromSatker = uploaderLevelNumber === 3;
const {
control,
@ -133,6 +139,11 @@ export default function FormDetailLiveReport() {
const details = response?.data?.data;
setDetail(details);
if (details?.uploaderLevelNumber !== undefined) {
setUploaderLevelNumber(details.uploaderLevelNumber);
}
if (details) {
setDate({
from: parseISO(details.startDate),
@ -176,21 +187,71 @@ export default function FormDetailLiveReport() {
statusId: Number(status),
message: description,
isPublish: status === "2",
placements: schedulePlacements?.filter((val) => val != "all")?.join(","),
// placements: schedulePlacements?.filter((val) => val !== "all")?.join(","),
placements: isScheduleFromSatker
? "satker"
: schedulePlacements?.filter((val) => val !== "all")?.join(","),
};
loading();
const response = await postApprovalSchedule(data);
close();
setModalOpen(false);
if (response?.error) {
error(response?.message);
return false;
error(response?.message || "Gagal menyimpan data");
return;
}
initState();
return false;
// ✅ update UI lokal (optimistic)
setDetail((prev) =>
prev
? {
...prev,
statusId: Number(status),
}
: prev,
);
Swal.fire({
icon: "success",
title: "Berhasil",
text:
status === "2"
? "Jadwal berhasil disetujui"
: status === "3"
? "Jadwal dikembalikan untuk revisi"
: "Jadwal berhasil ditolak",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/contributor/schedule/live-report");
}
});
}
// async function save() {
// const data = {
// scheduleId: Number(id),
// statusId: Number(status),
// message: description,
// isPublish: status === "2",
// placements: schedulePlacements?.filter((val) => val != "all")?.join(","),
// };
// loading();
// const response = await postApprovalSchedule(data);
// close();
// setModalOpen(false);
// if (response?.error) {
// error(response?.message);
// return false;
// }
// initState();
// return false;
// }
const [schedulePlacements, setSchedulePlacements] = useState<string[]>([]);
const setupPlacement = (placement: string, checked: boolean) => {
@ -224,6 +285,15 @@ export default function FormDetailLiveReport() {
setSchedulePlacements(temp);
};
const isCreator = Number(detail?.uploadedById) === Number(userId);
const isApprover =
Number(detail?.needApprovalFrom) === Number(userLevelId) &&
Number(userLevelNumber) < 2;
const isAlreadyProcessed =
detail?.statusId === 2 || detail?.statusId === 3 || detail?.statusId === 4;
return (
<div className="flex flex-col lg:flex-row gap-2">
<Card className="w-full lg:w-9/12">
@ -284,7 +354,7 @@ export default function FormDetailLiveReport() {
variant={"outline"}
className={cn(
"w-[280px] lg:w-[250px] justify-start text-left font-normal px-0 md:px-0 lg:px-4",
!date && "text-muted-foreground"
!date && "text-muted-foreground",
)}
>
<CalendarIcon size={15} className="mr-3" />
@ -494,7 +564,38 @@ export default function FormDetailLiveReport() {
</AccordionItem>
</Accordion>
{Number(detail?.needApprovalFrom) == Number(userLevelId) &&
{(isApprover || isCreator) && !isAlreadyProcessed && (
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
color="primary"
type="button"
>
<Icon icon="fa:check" className="mr-3" />
{t("accept", { defaultValue: "Accept" })}
</Button>
<Button
onClick={() => actionApproval("3")}
className="bg-orange-400 hover:bg-orange-300"
type="button"
>
<Icon icon="fa:comment-o" className="mr-3" />
{t("revision", { defaultValue: "Revision" })}
</Button>
<Button
onClick={() => actionApproval("4")}
color="destructive"
type="button"
>
<Icon icon="fa:times" className="mr-3" />
{t("reject", { defaultValue: "Reject" })}
</Button>
</div>
)}
{/* {Number(detail?.needApprovalFrom) == Number(userLevelId) &&
Number(userLevelNumber) < 2 ? (
Number(detail?.uploadedById) == Number(userId) ? (
""
@ -527,13 +628,15 @@ export default function FormDetailLiveReport() {
)
) : (
""
)}
)} */}
</Card>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="overflow-y-auto">
<DialogHeader>
<DialogTitle>{t("leave-comment", { defaultValue: "Leave Comment" })}</DialogTitle>
<DialogTitle>
{t("leave-comment", { defaultValue: "Leave Comment" })}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-1 text-sm">
<p>
@ -544,18 +647,19 @@ export default function FormDetailLiveReport() {
status === "2"
? "text-primary"
: status === "3"
? "text-warning"
: "text-destructive"
? "text-warning"
: "text-destructive"
}
>
{status === "2"
? "Disetujui"
: status === "3"
? "Revisi"
: "Ditolak"}
? "Revisi"
: "Ditolak"}
</span>
</p>
{status === "2" && (
{/* {status === "2" && ( */}
{status === "2" && !isScheduleFromSatker && (
<div className="flex flex-row gap-2">
<div className="flex items-center space-x-2">
<Checkbox
@ -630,9 +734,15 @@ export default function FormDetailLiveReport() {
type="button"
color="primary"
onClick={() => submit()}
// disabled={
// description.length < 1 ||
// (schedulePlacements.length < 1 && status === "2")
// }
disabled={
description.length < 1 ||
(schedulePlacements.length < 1 && status === "2")
(!isScheduleFromSatker &&
schedulePlacements.length < 1 &&
status === "2")
}
>
{t("submit", { defaultValue: "Submit" })}

View File

@ -530,7 +530,7 @@ export function TambahIklanDetail() {
</p>
<Image
src={`https://netidhub.com/api/advertisements/viewer/${id}`}
src={`https://new.netidhub.com/api/advertisements/viewer/${id}`}
alt="Thumbnail Gambar Utama"
className=" rounded-md my-3"
width={300}

View File

@ -296,7 +296,7 @@ export function TambahIklanUpdate() {
formMedia.append("file", imageFiles[0]);
} else if (detail?.id) {
const existingFile = await fetchExistingImageAsFile(
`https://netidhub.com/api/advertisements/viewer/${detail.id}`,
`https://new.netidhub.com/api/advertisements/viewer/${detail.id}`,
"existing-image.jpg"
);
formMedia.append("file", existingFile);
@ -631,7 +631,7 @@ export function TambahIklanUpdate() {
))}
<Image
src={`https://netidhub.com/api/advertisements/viewer/${id}`}
src={`https://new.netidhub.com/api/advertisements/viewer/${id}`}
alt="Thumbnail Gambar Utama"
className=" rounded-md my-3"
width={300}

View File

@ -43,7 +43,7 @@ import { getCsrfToken } from "@/service/auth";
import { loading } from "@/lib/swal";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import { cn } from "@/lib/utils";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
Popover,
PopoverContent,
@ -98,7 +98,7 @@ const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
{ ssr: false },
);
export default function FormTaskTa() {
@ -136,7 +136,7 @@ export default function FormTaskTa() {
const [userLevels, setUserLevels] = useState<any>();
const [userCompetencies, setUserCompetencies] = useState<any[]>([]);
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set()
new Set(),
);
const [listExpert, setListExpert] = useState<any[]>([]);
const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set());
@ -181,6 +181,33 @@ export default function FormTaskTa() {
mode: "all",
});
const [profile, setProfile] = useState<any>(null);
const userLevelId = Number(getCookiesDecrypt("ulie"));
const roleId = Number(getCookiesDecrypt("urie"));
const userId = Number(getCookiesDecrypt("uie"));
const MABES_LEVEL_ID = 216; // userLevelId Mabes Polri
const APPROVER_ROLE_ID = 3; // roleId Approver
const isMabes = userLevelId === MABES_LEVEL_ID;
const isApprover = roleId === APPROVER_ROLE_ID;
const isMabesApprover =
userLevelId === MABES_LEVEL_ID && roleId === APPROVER_ROLE_ID;
const shouldHideExpert = isMabes && isApprover;
useEffect(() => {
async function fetchUserLevel() {
try {
const res = await getUserLevelForAssignments();
setProfile(res?.data?.data);
} catch (e) {
console.error("Failed fetch user level", e);
}
}
fetchUserLevel();
}, []);
useEffect(() => {
getDataAdditional();
}, []);
@ -218,7 +245,7 @@ export default function FormTaskTa() {
}
const uniqueExperts = Array.from(
new Map(allExperts.map((e) => [e.id, e])).values()
new Map(allExperts.map((e) => [e.id, e])).values(),
);
setListExpert(uniqueExperts);
@ -352,55 +379,124 @@ export default function FormTaskTa() {
// // });
// };
// const save = async (data: TaskSchema) => {
// const cleanedLinks = links
// .map((link) => link.trim())
// .filter((link) => link.startsWith("http"));
// const requestData = {
// ...data,
// // assignedToUsers: handleExpertChange(),
// assignedToUsers: isMabesApprover ? "464" : handleExpertChange(),
// assignmentType: taskType,
// assignmentTypeId: type,
// expertCompetencies: Array.from(selectedCompetencies).join(","),
// attachmentUrl: cleanedLinks,
// };
// console.log("FINAL ASSIGNED TO:", {
// isMabesApprover,
// assignedToUsers: isMabesApprover
// ? String(roleId)
// : handleExpertChange(),
// });
// const response = await createTaskTa(requestData);
// const id = String(response?.data?.data.id);
// // Set block table TA
// localStorage.setItem("TA_UPLOAD_IN_PROGRESS", "true");
// loading(); // SHOW SWAL LOADING
// // Kumpulkan semua upload
// const allUploads: Promise<any>[] = [];
// imageFiles.forEach((item, idx) =>
// allUploads.push(uploadResumableFile(idx, id, item, "1", "0"))
// );
// videoFiles.forEach((item, idx) =>
// allUploads.push(uploadResumableFile(idx, id, item, "2", "0"))
// );
// textFiles.forEach((item, idx) =>
// allUploads.push(uploadResumableFile(idx, id, item, "3", "0"))
// );
// audioFiles.forEach((item, idx) =>
// allUploads.push(uploadResumableFile(idx, id, item, "4", "0"))
// );
// // Tunggu upload selesai
// await Promise.all(allUploads);
// // Hapus flag
// localStorage.removeItem("TA_UPLOAD_IN_PROGRESS");
// // Close loading + redirect
// successSubmit("/in/contributor/task-ta");
// };
const save = async (data: TaskSchema) => {
const cleanedLinks = links
.map((link) => link.trim())
.filter((link) => link.startsWith("http"));
try {
loading();
const requestData = {
...data,
assignedToUsers: handleExpertChange(),
assignmentType: taskType,
assignmentTypeId: type,
expertCompetencies: Array.from(selectedCompetencies).join(","),
attachmentUrl: cleanedLinks,
};
const cleanedLinks = links
.map((link) => link.trim())
.filter((link) => link.startsWith("http"));
const response = await createTaskTa(requestData);
const id = String(response?.data?.data.id);
const requestData = {
...data,
// assignedToUsers: isMabesApprover ? "464" : handleExpertChange(),
assignedToUsers: isMabesApprover ? "464,8258" : handleExpertChange(),
assignmentType: taskType,
assignmentTypeId: type,
expertCompetencies: Array.from(selectedCompetencies).join(","),
attachmentUrl: cleanedLinks,
};
// Set block table TA
localStorage.setItem("TA_UPLOAD_IN_PROGRESS", "true");
const response = await createTaskTa(requestData);
loading(); // SHOW SWAL LOADING
if (!response?.data?.data?.id) {
throw new Error("Gagal membuat task");
}
// Kumpulkan semua upload
const allUploads: Promise<any>[] = [];
const assignmentId = String(response.data.data.id);
imageFiles.forEach((item, idx) =>
allUploads.push(uploadResumableFile(idx, id, item, "1", "0"))
);
const uploads: Promise<any>[] = [];
videoFiles.forEach((item, idx) =>
allUploads.push(uploadResumableFile(idx, id, item, "2", "0"))
);
imageFiles.forEach((file, i) =>
uploads.push(uploadResumableFile(i, assignmentId, file, "1", "0")),
);
textFiles.forEach((item, idx) =>
allUploads.push(uploadResumableFile(idx, id, item, "3", "0"))
);
videoFiles.forEach((file, i) =>
uploads.push(uploadResumableFile(i, assignmentId, file, "2", "0")),
);
audioFiles.forEach((item, idx) =>
allUploads.push(uploadResumableFile(idx, id, item, "4", "0"))
);
textFiles.forEach((file, i) =>
uploads.push(uploadResumableFile(i, assignmentId, file, "3", "0")),
);
// Tunggu upload selesai
await Promise.all(allUploads);
audioFiles.forEach((file, i) =>
uploads.push(uploadResumableFile(i, assignmentId, file, "4", "0")),
);
// Hapus flag
localStorage.removeItem("TA_UPLOAD_IN_PROGRESS");
await Promise.all(uploads);
// Close loading + redirect
successSubmit("/in/contributor/task-ta");
successSubmit("/in/contributor/task-ta");
} catch (err: any) {
console.error("SUBMIT ERROR:", err);
Swal.fire({
icon: "error",
title: "Gagal",
text:
err?.response?.data?.message ||
err?.message ||
"Terjadi kesalahan, data tidak tersimpan",
});
}
};
const onSubmit = (data: TaskSchema) => {
@ -461,7 +557,7 @@ export default function FormTaskTa() {
// Convert Blob to File and add preview
const fileWithPreview: FileWithPreview = Object.assign(
new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
{ preview: url },
);
// Add to state
@ -543,46 +639,91 @@ export default function FormTaskTa() {
// upload.start();
// }
// function uploadResumableFile(
// idx: number,
// id: string,
// file: any,
// fileTypeId: string,
// duration: string
// ) {
// return new Promise(async (resolve, reject) => {
// const resCsrf = await getCsrfToken();
// const csrfToken = resCsrf?.data?.token;
// const upload = new Upload(file, {
// endpoint: `${process.env.NEXT_PUBLIC_API}/assignment-expert/file/upload`,
// headers: { "X-XSRF-TOKEN": csrfToken },
// retryDelays: [0, 3000, 6000, 12000],
// chunkSize: 20000,
// metadata: {
// assignmentId: id,
// filename: file.name,
// contentType: file.type,
// fileTypeId,
// duration,
// },
// onBeforeRequest(req) {
// req.getUnderlyingObject().withCredentials = true;
// },
// onError(err) {
// console.error("Upload error:", err);
// reject(err);
// },
// onSuccess() {
// console.log("Upload selesai:", file.name);
// resolve(true);
// },
// });
// upload.start();
// });
// }
function uploadResumableFile(
idx: number,
id: string,
file: any,
file: File,
fileTypeId: string,
duration: string
duration: string,
) {
return new Promise(async (resolve, reject) => {
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
try {
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/assignment-expert/file/upload`,
headers: { "X-XSRF-TOKEN": csrfToken },
retryDelays: [0, 3000, 6000, 12000],
chunkSize: 20000,
metadata: {
assignmentId: id,
filename: file.name,
contentType: file.type,
fileTypeId,
duration,
},
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/assignment-expert/file/upload`,
headers: { "X-XSRF-TOKEN": csrfToken },
retryDelays: [0, 3000, 6000],
chunkSize: 20000,
metadata: {
assignmentId: id,
filename: file.name,
contentType: file.type,
fileTypeId,
duration,
},
onBeforeRequest(req) {
req.getUnderlyingObject().withCredentials = true;
},
onBeforeRequest(req) {
req.getUnderlyingObject().withCredentials = true;
},
onError(err) {
console.error("Upload error:", err);
reject(err);
},
onError(error) {
reject(error);
},
onSuccess() {
console.log("Upload selesai:", file.name);
resolve(true);
},
});
onSuccess() {
resolve(true);
},
});
upload.start();
upload.start();
} catch (err) {
reject(err);
}
});
}
@ -671,12 +812,29 @@ export default function FormTaskTa() {
value={taskType}
onValueChange={(value) => setTaskType(String(value))}
className="flex flex-wrap gap-3"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="atensi-khusus" id="khusus" />
<Label htmlFor="khusus">Atensi Khusus</Label>
</div>
{!isMabes && (
<div className="flex items-center gap-2">
<RadioGroupItem value="tugas-harian" id="harian" />
<Label htmlFor="harian">Tugas Harian</Label>
</div>
)}
</RadioGroup>
{/* <RadioGroup
value={taskType}
onValueChange={(value) => setTaskType(String(value))}
className="flex flex-wrap gap-3"
>
<RadioGroupItem value="atensi-khusus" id="khusus" />
<Label htmlFor="atensi-khusus">Atensi Khusus</Label>
<RadioGroupItem value="tugas-harian" id="harian" />
<Label htmlFor="tugas-harian">Tugas Harian</Label>
</RadioGroup>
</RadioGroup> */}
</div>
<div className="flex flex-col space-y-2 mt-5">
<Label className="mr-3 mb-1">Tanggal</Label>
@ -688,7 +846,7 @@ export default function FormTaskTa() {
variant={"outline"}
className={cn(
"w-[280px] lg:w-[250px] justify-start text-left font-normal border border-slate-300 px-0 md:px-0 lg:px-4",
!date && "text-muted-foreground"
!date && "text-muted-foreground",
)}
>
<CalendarIcon size={15} className="mr-3" />
@ -718,101 +876,109 @@ export default function FormTaskTa() {
</PopoverContent>
</Popover>
</div>
<div className="mt-5 space-y-2">
<Label>
{t("areas-expertise", { defaultValue: "Areas Expertise" })}
</Label>
<div className="flex flex-wrap gap-4">
{userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}>
<Checkbox
id={`comp-${item.id}`}
checked={selectedCompetencies.has(item.id)}
onCheckedChange={() => handleCompetencyChange(item.id)}
/>
<Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
</div>
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>
{t("choose-expert", { defaultValue: "Choose Expert" })}
</Label>
<div className="flex flex-wrap gap-4">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{"Pilih Tenaga Ahli"}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>Daftar Tenaga Ahli</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listExpert?.map((expert: any) => (
<div key={expert.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(expert.id)}
onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
<div className="flex flex-col gap-2">
<div className="font-bold">{expert.fullname}</div>
<div className="italic">({expert.username})</div>
</div>
</Label>
</div>
))}
{!isMabesApprover && (
<div className="mt-5 space-y-2">
<Label>
{t("areas-expertise", { defaultValue: "Areas Expertise" })}
</Label>
<div className="flex flex-wrap gap-4">
{userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}>
<Checkbox
id={`comp-${item.id}`}
checked={selectedCompetencies.has(item.id)}
onCheckedChange={() => handleCompetencyChange(item.id)}
/>
<Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
</div>
</DialogContent>
</Dialog>
</div>
{checkedLevels.size > 0 && (
<div className="mt-3">
<Label className="text-sm text-gray-600 mb-2 block">
Tenaga Ahli Terpilih ({checkedLevels.size})
</Label>
<div className="flex flex-wrap gap-2">
{Array.from(checkedLevels).map((expertId) => {
const expert = listExpert?.find(
(exp: any) => exp.id === expertId
);
return expert ? (
<div
key={expert.id}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full border border-blue-200"
>
<span>{expert.fullname}</span>
<button
type="button"
onClick={() => handleCheckboxChange(expert.id)}
className="ml-1 text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-0.5 transition-colors"
title="Remove expert"
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
) : null;
})}
</div>
))}
</div>
)}
</div>
</div>
)}
{!isMabesApprover && (
<div className="mt-5 space-y-2">
<Label>
{t("choose-expert", { defaultValue: "Choose Expert" })}
</Label>
<div className="flex flex-wrap gap-4">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{"Pilih Tenaga Ahli"}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>Daftar Tenaga Ahli</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listExpert?.map((expert: any) => (
<div key={expert.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(expert.id)}
onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
<div className="flex flex-col gap-2">
<div className="font-bold">
{expert.fullname}
</div>
<div className="italic">
({expert.username})
</div>
</div>
</Label>
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
{checkedLevels.size > 0 && (
<div className="mt-3">
<Label className="text-sm text-gray-600 mb-2 block">
Tenaga Ahli Terpilih ({checkedLevels.size})
</Label>
<div className="flex flex-wrap gap-2">
{Array.from(checkedLevels).map((expertId) => {
const expert = listExpert?.find(
(exp: any) => exp.id === expertId,
);
return expert ? (
<div
key={expert.id}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full border border-blue-200"
>
<span>{expert.fullname}</span>
<button
type="button"
onClick={() => handleCheckboxChange(expert.id)}
className="ml-1 text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-0.5 transition-colors"
title="Remove expert"
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
) : null;
})}
</div>
</div>
)}
</div>
)}
<div className="mt-5 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Controller
@ -946,7 +1112,9 @@ export default function FormTaskTa() {
<Input
type="url"
className="border rounded p-2 w-full"
placeholder={`Masukkan link berita ${index + 1}`}
placeholder={`Masukkan link berita ${
index + 1
} | Contoh: https://www.detik.com`}
value={link}
onChange={(e) =>
handleLinkChange(index, e.target.value)

View File

@ -80,7 +80,8 @@
// };
// export default AdvertisementPlacements;
import { listDataAdvertisements } from "@/service/broadcast/broadcast";
"use client";
import { useEffect, useState } from "react";
import * as React from "react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
@ -93,35 +94,40 @@ interface Advertisement {
[key: string]: any;
}
const AdvertisementPlacements = (props: {
interface AdvertisementPlacementsProps {
placement: string;
data: Advertisement[];
data?: Advertisement[]; // ⬅️ PENTING: optional
loading: boolean;
}) => {
const [ads, setAds] = useState<Advertisement[] | undefined[]>([]);
}
const AdvertisementPlacements = ({
placement,
data = [], // ⬅️ DEFAULT VALUE (KUNCI UTAMA)
loading,
}: AdvertisementPlacementsProps) => {
const [ads, setAds] = useState<(Advertisement | undefined)[]>([]);
useEffect(() => {
if (!props.loading && props.data.length > 0) {
const filtered = props.data.filter((a) =>
a.placements.includes(props.placement)
);
if (loading || data.length === 0) return;
let temps: Advertisement[] | undefined[] = [];
temps[0] = filtered.find((b) => b.placements.includes("top"));
temps[1] = filtered.find((b) => b.placements.includes("bottom"));
setAds(temps);
}
}, [props.data, props.loading, props.placement]);
const filtered = data.filter((a) => a.placements?.includes(placement));
const temps: (Advertisement | undefined)[] = [];
temps[0] = filtered.find((b) => b.placements?.includes("top"));
temps[1] = filtered.find((b) => b.placements?.includes("bottom"));
setAds(temps);
}, [data, loading, placement]);
return (
<div
className={`sticky top-0 space-y-4 ${
props.placement === "left" ? "ml-14" : "mr-14"
placement === "left" ? "ml-14" : "mr-14"
}`}
>
{props.loading && <p className="text-sm text-gray-500">Loading...</p>}
{loading && <p className="text-sm text-gray-500">Loading...</p>}
{ads?.map(
{ads.map(
(ad) =>
ad && (
<Dialog key={ad.id}>
@ -132,6 +138,7 @@ const AdvertisementPlacements = (props: {
className="w-full cursor-pointer rounded-lg shadow-md hover:opacity-90 transition"
/>
</DialogTrigger>
<DialogContent className="max-w-4xl p-0 bg-transparent border-0 shadow-none">
<img
src={ad.contentFileUrl || ad.imageUrl}

View File

@ -63,7 +63,7 @@ type HeroModalProps = {
// useEffect(() => {
// async function fetchCategories() {
// const url = "https://netidhub.com/api/csrf";
// const url = "https://new.netidhub.com/api/csrf";
// try {
// const response = await fetch(url);

View File

@ -50,7 +50,7 @@ const HeroModal = ({ onClose }: { onClose: () => void }) => {
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);
@ -268,7 +268,7 @@ const Hero = (props: { group?: string }) => {
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);

View File

@ -83,7 +83,7 @@ const NewContent = (props: { group: string; type: string }) => {
? "1"
: selectedTab == "video"
? "2"
: selectedTab == "text"
: selectedTab == "document"
? "3"
: selectedTab == "audio"
? "4"

View File

@ -36,7 +36,7 @@ const ScrollableContentPolda = () => {
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);

View File

@ -35,7 +35,7 @@ const ScrollableContentSatker = () => {
: "";
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);

View File

@ -39,7 +39,7 @@ const ScrollableContent = () => {
useEffect(() => {
async function fetchCategories() {
const url = "https://netidhub.com/api/csrf";
const url = "https://new.netidhub.com/api/csrf";
try {
const response = await fetch(url);

View File

@ -31,7 +31,7 @@ const DashCodeFooter = () => {
>
<div className="h-[50px] w-[50px] rounded-full relative left-[0px] top-[0px] custom-dropshadow">
<Image
src={"https://netidhub.com/assets/img/user-avatar.png"}
src={"https://new.netidhub.com/assets/img/user-avatar.png"}
alt={"Image"}
width={50}
height={50}

View File

@ -168,6 +168,13 @@ export function getMenuList(pathname: string, t: any): Group[] {
icon: "heroicons:shopping-cart",
children: [],
},
{
href: "/contributor/task-ta",
label: "penugasan TA",
active: pathname.includes("/contributor/task-ta"),
icon: "heroicons:shopping-cart",
children: [],
},
],
},
],
@ -2422,7 +2429,10 @@ export function getMenuList(pathname: string, t: any): Group[] {
},
];
}
} else if (Number(roleId) == 4 && Number(levelNumber) == 3) {
} else if (
Number(roleId) == 4 ||
(Number(roleId) == 3 && Number(levelNumber) == 3)
) {
menusSelected = [
{
groupLabel: t("apps"),
@ -2509,35 +2519,35 @@ export function getMenuList(pathname: string, t: any): Group[] {
},
],
},
{
groupLabel: "",
id: "planning",
menus: [
{
id: "planning",
href: "/contributor/planning",
label: t("planning"),
active: pathname.includes("/planning"),
icon: "pajamas:planning",
submenus: [
{
href: "/contributor/planning/mediahub",
label: "mediaHub",
active: pathname.includes("/planning/mediahub"),
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/contributor/planning/medsos-mediahub",
label: "medsos mediahub",
active: pathname.includes("/planning/medsos-mediahub"),
icon: "heroicons:shopping-cart",
children: [],
},
],
},
],
},
// {
// groupLabel: "",
// id: "planning",
// menus: [
// {
// id: "planning",
// href: "/contributor/planning",
// label: t("planning"),
// active: pathname.includes("/planning"),
// icon: "pajamas:planning",
// submenus: [
// {
// href: "/contributor/planning/mediahub",
// label: "mediaHub",
// active: pathname.includes("/planning/mediahub"),
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/contributor/planning/medsos-mediahub",
// label: "medsos mediahub",
// active: pathname.includes("/planning/medsos-mediahub"),
// icon: "heroicons:shopping-cart",
// children: [],
// },
// ],
// },
// ],
// },
{
groupLabel: "",
id: "task",
@ -2564,44 +2574,51 @@ export function getMenuList(pathname: string, t: any): Group[] {
icon: "uil:schedule",
submenus: [
{
href: "/contributor/schedule/press-conference",
label: t("press-conference"),
active: pathname.includes("/schedule/press-conference"),
href: "/contributor/schedule/live-report",
label: t("live-report"),
active: pathname.includes("/schedule/live-report"),
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/contributor/schedule/event",
label: t("event"),
active: pathname.includes("/schedule/event"),
icon: "heroicons:shopping-cart",
children: [],
},
{
href: "/contributor/schedule/press-release",
label: t("press-release"),
active: pathname.includes("/schedule/press-release"),
icon: "heroicons:shopping-cart",
children: [],
},
// {
// href: "/contributor/schedule/press-conference",
// label: t("press-conference"),
// active: pathname.includes("/schedule/press-conference"),
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/contributor/schedule/event",
// label: t("event"),
// active: pathname.includes("/schedule/event"),
// icon: "heroicons:shopping-cart",
// children: [],
// },
// {
// href: "/contributor/schedule/press-release",
// label: t("press-release"),
// active: pathname.includes("/schedule/press-release"),
// icon: "heroicons:shopping-cart",
// children: [],
// },
],
},
],
},
{
groupLabel: "",
id: "blog",
menus: [
{
id: "blog",
href: "/contributor/blog",
label: t("blog"),
active: pathname.includes("/blog"),
icon: "fluent:clipboard-text-32-regular",
submenus: [],
},
],
},
// {
// groupLabel: "",
// id: "blog",
// menus: [
// {
// id: "blog",
// href: "/contributor/blog",
// label: t("blog"),
// active: pathname.includes("/blog"),
// icon: "fluent:clipboard-text-32-regular",
// submenus: [],
// },
// ],
// },
{
groupLabel: "",
id: "curatedcontent",
@ -2649,8 +2666,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
(Number(roleId) == 3 || Number(roleId) == 14 || Number(roleId) == 15) &&
Number(levelNumber) == 3
) {
if (Number(userParentLevelId) != 761)
{
if (Number(userParentLevelId) != 761) {
menusSelected = [
{
groupLabel: t("apps"),
@ -5209,7 +5225,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-area",
label: t("areaCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-area"
"/charts/appex-charts/charts-appex-area",
),
children: [],
},
@ -5217,7 +5233,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-bar",
label: t("barCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-bar"
"/charts/appex-charts/charts-appex-bar",
),
children: [],
},
@ -5225,7 +5241,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-boxplot",
label: t("boxplotCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-boxplot"
"/charts/appex-charts/charts-appex-boxplot",
),
children: [],
},
@ -5233,7 +5249,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-bubble",
label: t("bubbleCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-bubble"
"/charts/appex-charts/charts-appex-bubble",
),
children: [],
},
@ -5241,7 +5257,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-candlestick",
label: t("candlestickCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-candlestick"
"/charts/appex-charts/charts-appex-candlestick",
),
children: [],
},
@ -5249,7 +5265,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-column",
label: t("columnCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-column"
"/charts/appex-charts/charts-appex-column",
),
children: [],
},
@ -5257,7 +5273,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-combo",
label: t("comboCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-combo"
"/charts/appex-charts/charts-appex-combo",
),
children: [],
},
@ -5266,7 +5282,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-funnel",
label: t("funnelCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-funnel"
"/charts/appex-charts/charts-appex-funnel",
),
children: [],
},
@ -5274,7 +5290,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-heatmap",
label: t("heatmapCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-heatmap"
"/charts/appex-charts/charts-appex-heatmap",
),
children: [],
},
@ -5282,7 +5298,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-line",
label: t("lineCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-line"
"/charts/appex-charts/charts-appex-line",
),
children: [],
},
@ -5290,7 +5306,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-pie",
label: t("pieCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-pie"
"/charts/appex-charts/charts-appex-pie",
),
children: [],
},
@ -5298,7 +5314,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-polararea",
label: t("ploarareaCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-polararea"
"/charts/appex-charts/charts-appex-polararea",
),
children: [],
},
@ -5306,7 +5322,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-radar",
label: t("radarCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-radar"
"/charts/appex-charts/charts-appex-radar",
),
children: [],
},
@ -5314,7 +5330,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-radialbars",
label: t("radialbarCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-radialbars"
"/charts/appex-charts/charts-appex-radialbars",
),
children: [],
},
@ -5322,7 +5338,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-range",
label: t("rangeCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-range"
"/charts/appex-charts/charts-appex-range",
),
children: [],
},
@ -5330,7 +5346,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-scatter",
label: t("scatterCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-scatter"
"/charts/appex-charts/charts-appex-scatter",
),
children: [],
},
@ -5338,7 +5354,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-timeline",
label: t("timelineCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-timeline"
"/charts/appex-charts/charts-appex-timeline",
),
children: [],
},
@ -5346,7 +5362,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/appex-charts/charts-appex-treemap",
label: t("treemapCharts"),
active: pathname.includes(
"/charts/appex-charts/charts-appex-treemap"
"/charts/appex-charts/charts-appex-treemap",
),
children: [],
},
@ -5362,7 +5378,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-area",
label: t("areaCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-area"
"/charts/rechart/charts-rechart-area",
),
children: [],
},
@ -5370,7 +5386,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-bar",
label: t("barCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-bar"
"/charts/rechart/charts-rechart-bar",
),
children: [],
},
@ -5378,7 +5394,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-composed",
label: t("composedCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-composed"
"/charts/rechart/charts-rechart-composed",
),
children: [],
},
@ -5386,7 +5402,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-line",
label: t("lineCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-line"
"/charts/rechart/charts-rechart-line",
),
children: [],
},
@ -5394,7 +5410,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-pie",
label: t("pieCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-pie"
"/charts/rechart/charts-rechart-pie",
),
children: [],
},
@ -5402,7 +5418,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-radar",
label: t("radarCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-radar"
"/charts/rechart/charts-rechart-radar",
),
children: [],
},
@ -5410,7 +5426,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-radialbar",
label: t("radialbarCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-radialbar"
"/charts/rechart/charts-rechart-radialbar",
),
children: [],
},
@ -5418,7 +5434,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-scatter",
label: t("scatterCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-scatter"
"/charts/rechart/charts-rechart-scatter",
),
children: [],
},
@ -5426,7 +5442,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/rechart/charts-rechart-treemap",
label: t("treemapCharts"),
active: pathname.includes(
"/charts/rechart/charts-rechart-treemap"
"/charts/rechart/charts-rechart-treemap",
),
children: [],
},
@ -5442,7 +5458,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-area",
label: t("areaCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-area"
"/charts/chart-js/charts-chartjs-area",
),
children: [],
},
@ -5450,7 +5466,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-bar",
label: t("barCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-bar"
"/charts/chart-js/charts-chartjs-bar",
),
children: [],
},
@ -5458,7 +5474,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-line",
label: t("lineCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-line"
"/charts/chart-js/charts-chartjs-line",
),
children: [],
},
@ -5466,7 +5482,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-animations",
label: t("animationCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-animations"
"/charts/chart-js/charts-chartjs-animations",
),
children: [],
},
@ -5474,7 +5490,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-legend",
label: t("legendCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-legend"
"/charts/chart-js/charts-chartjs-legend",
),
children: [],
},
@ -5482,7 +5498,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-scaleoptions",
label: t("scaleOptionCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-scaleoptions"
"/charts/chart-js/charts-chartjs-scaleoptions",
),
children: [],
},
@ -5490,7 +5506,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-scales",
label: t("scaleCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-scales"
"/charts/chart-js/charts-chartjs-scales",
),
children: [],
},
@ -5498,7 +5514,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-scriptable",
label: t("scriptableCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-scriptable"
"/charts/chart-js/charts-chartjs-scriptable",
),
children: [],
},
@ -5506,7 +5522,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-title",
label: t("titleCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-title"
"/charts/chart-js/charts-chartjs-title",
),
children: [],
},
@ -5514,7 +5530,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-tooltip",
label: t("tooltipChart"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-tooltip"
"/charts/chart-js/charts-chartjs-tooltip",
),
children: [],
},
@ -5522,7 +5538,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
href: "/charts/chart-js/charts-chartjs-other",
label: t("otherCharts"),
active: pathname.includes(
"/charts/chart-js/charts-chartjs-other"
"/charts/chart-js/charts-chartjs-other",
),
children: [],
},

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,35 @@ const nextConfig = {
// locales: ["en", "in"],
// defaultLocale: "in",
// },
// images: {
// remotePatterns: [
// {
// protocol: "https",
// hostname: "api.lorem.space",
// },
// {
// protocol: "https",
// hostname: "lh3.googleusercontent.com",
// },
// {
// protocol: "https",
// hostname: "a0.muscache.com",
// },
// {
// protocol: "https",
// hostname: "avatars.githubusercontent.com",
// },
// {
// protocol: "https",
// hostname: "i.pravatar.cc",
// },
// { protocol: "https", hostname: "netidhub.com" },
// {
// protocol: "https",
// hostname: "netidhub.com",
// },
// ],
// },
images: {
remotePatterns: [
{
@ -42,11 +71,15 @@ const nextConfig = {
protocol: "https",
hostname: "i.pravatar.cc",
},
{ protocol: "https", hostname: "netidhub.com" },
{
protocol: "https",
hostname: "netidhub.com",
},
{
protocol: "https",
hostname: "new.netidhub.com",
pathname: "/**",
},
],
},
// eslint: {

237
package-lock.json generated
View File

@ -80,6 +80,7 @@
"dayjs": "^1.11.11",
"embla-carousel-autoplay": "^8.1.3",
"embla-carousel-react": "^8.1.3",
"file-saver": "^2.0.5",
"framer-motion": "^11.15.0",
"geojson": "^0.5.0",
"html-react-parser": "^5.2.0",
@ -137,6 +138,7 @@
"uuid": "^13.0.0",
"vaul": "^0.9.1",
"wavesurfer.js": "^7.8.16",
"xlsx": "^0.18.5",
"yup": "^1.6.1",
"zod": "^3.23.8"
},
@ -156,14 +158,15 @@
"@types/react": "^18.3.13",
"@types/react-geocode": "^0.2.4",
"@types/rtl-detect": "^1.0.3",
"autoprefixer": "^10.4.24",
"cross-env": "^7.0.3",
"d3-shape": "^3.2.0",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"jest": "^30.0.4",
"jest-environment-jsdom": "^30.0.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "^5"
}
},
@ -23691,6 +23694,15 @@
"node": ">=0.4.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@ -24146,6 +24158,43 @@
"npm": ">=6.4.1"
}
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -24338,6 +24387,18 @@
}
]
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -24421,9 +24482,9 @@
}
},
"node_modules/browserslist": {
"version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
@ -24438,11 +24499,13 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@ -24696,9 +24759,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"funding": [
{
"type": "opencollective",
@ -24712,7 +24775,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
@ -24742,6 +24806,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -25214,6 +25291,15 @@
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
"integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@ -25417,6 +25503,18 @@
"node": ">=10"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@ -26677,9 +26775,10 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA=="
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
"license": "ISC"
},
"node_modules/elkjs": {
"version": "0.9.3",
@ -27871,6 +27970,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
@ -28101,6 +28206,29 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.18.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
@ -34197,9 +34325,10 @@
"dev": true
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT"
},
"node_modules/non-layered-tidy-tree-layout": {
"version": "2.0.2",
@ -34972,6 +35101,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -38523,6 +38653,18 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/ssri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@ -39231,9 +39373,10 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -39243,7 +39386,7 @@
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
@ -39252,7 +39395,7 @@
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
@ -40319,9 +40462,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
@ -40336,6 +40479,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@ -41270,6 +41414,24 @@
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -41405,6 +41567,27 @@
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",

View File

@ -89,6 +89,7 @@
"dayjs": "^1.11.11",
"embla-carousel-autoplay": "^8.1.3",
"embla-carousel-react": "^8.1.3",
"file-saver": "^2.0.5",
"framer-motion": "^11.15.0",
"geojson": "^0.5.0",
"html-react-parser": "^5.2.0",
@ -146,6 +147,7 @@
"uuid": "^13.0.0",
"vaul": "^0.9.1",
"wavesurfer.js": "^7.8.16",
"xlsx": "^0.18.5",
"yup": "^1.6.1",
"zod": "^3.23.8"
},
@ -165,14 +167,15 @@
"@types/react": "^18.3.13",
"@types/react-geocode": "^0.2.4",
"@types/rtl-detect": "^1.0.3",
"autoprefixer": "^10.4.24",
"cross-env": "^7.0.3",
"d3-shape": "^3.2.0",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"jest": "^30.0.4",
"jest-environment-jsdom": "^30.0.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "^5"
}
}

View File

@ -10,7 +10,7 @@ importers:
dependencies:
'@ckeditor/ckeditor5-react':
specifier: ^6.2.0
version: 6.3.0(@ckeditor/ckeditor5-core@41.3.1)(@ckeditor/ckeditor5-editor-multi-root@47.1.0)(@ckeditor/ckeditor5-engine@41.3.1)(@ckeditor/ckeditor5-utils@41.3.1)(@ckeditor/ckeditor5-watchdog@41.3.1)(react@18.3.1)
version: 6.3.0(@ckeditor/ckeditor5-core@47.1.0)(@ckeditor/ckeditor5-editor-multi-root@47.1.0)(@ckeditor/ckeditor5-engine@47.1.0)(@ckeditor/ckeditor5-utils@47.1.0)(@ckeditor/ckeditor5-watchdog@47.1.0)(react@18.3.1)
'@dnd-kit/core':
specifier: ^6.1.0
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -224,6 +224,9 @@ importers:
embla-carousel-react:
specifier: ^8.1.3
version: 8.6.0(react@18.3.1)
file-saver:
specifier: ^2.0.5
version: 2.0.5
framer-motion:
specifier: ^11.15.0
version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -395,6 +398,9 @@ importers:
wavesurfer.js:
specifier: ^7.8.16
version: 7.11.0
xlsx:
specifier: ^0.18.5
version: 0.18.5
yup:
specifier: ^1.6.1
version: 1.7.1
@ -3033,6 +3039,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@ -3310,6 +3320,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@2.3.0:
resolution: {integrity: sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==}
engines: {node: '>=4'}
@ -3421,6 +3435,10 @@ packages:
code-block-writer@12.0.0:
resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collect-v8-coverage@1.0.3:
resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==}
@ -3542,6 +3560,11 @@ packages:
typescript:
optional: true
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@ -4262,6 +4285,9 @@ packages:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
file-selector@2.1.2:
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
engines: {node: '>= 12'}
@ -4331,6 +4357,10 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
framer-motion@11.18.2:
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
peerDependencies:
@ -6887,6 +6917,10 @@ packages:
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@ -7554,10 +7588,18 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -7601,6 +7643,11 @@ packages:
utf-8-validate:
optional: true
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@ -7940,8 +7987,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.1.0
'@ckeditor/ckeditor5-upload': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-alignment@41.3.1':
dependencies:
@ -8007,8 +8052,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-bookmark@47.1.0':
dependencies:
@ -8076,8 +8119,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@41.3.1':
dependencies:
@ -8144,8 +8185,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-decoupled@47.1.0':
dependencies:
@ -8155,8 +8194,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@47.1.0':
dependencies:
@ -8166,8 +8203,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.1.0':
dependencies:
@ -8467,8 +8502,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
'@ckeditor/ckeditor5-widget': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.1.0':
dependencies:
@ -8527,13 +8560,13 @@ snapshots:
'@ckeditor/ckeditor5-engine': 47.1.0
ckeditor5: 47.1.0
'@ckeditor/ckeditor5-react@6.3.0(@ckeditor/ckeditor5-core@41.3.1)(@ckeditor/ckeditor5-editor-multi-root@47.1.0)(@ckeditor/ckeditor5-engine@41.3.1)(@ckeditor/ckeditor5-utils@41.3.1)(@ckeditor/ckeditor5-watchdog@41.3.1)(react@18.3.1)':
'@ckeditor/ckeditor5-react@6.3.0(@ckeditor/ckeditor5-core@47.1.0)(@ckeditor/ckeditor5-editor-multi-root@47.1.0)(@ckeditor/ckeditor5-engine@47.1.0)(@ckeditor/ckeditor5-utils@47.1.0)(@ckeditor/ckeditor5-watchdog@47.1.0)(react@18.3.1)':
dependencies:
'@ckeditor/ckeditor5-core': 41.3.1
'@ckeditor/ckeditor5-core': 47.1.0
'@ckeditor/ckeditor5-editor-multi-root': 47.1.0
'@ckeditor/ckeditor5-engine': 41.3.1
'@ckeditor/ckeditor5-utils': 41.3.1
'@ckeditor/ckeditor5-watchdog': 41.3.1
'@ckeditor/ckeditor5-engine': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
'@ckeditor/ckeditor5-watchdog': 47.1.0
prop-types: 15.8.1
react: 18.3.1
@ -8544,8 +8577,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.1.0':
dependencies:
@ -8593,8 +8624,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-special-characters@47.1.0':
dependencies:
@ -8761,8 +8790,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@csstools/color-helpers@5.1.0': {}
@ -10900,6 +10927,8 @@ snapshots:
acorn@8.15.0: {}
adler-32@1.3.1: {}
agent-base@7.1.4: {}
ajv@6.12.6:
@ -11240,6 +11269,11 @@ snapshots:
ccount@2.0.1: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@2.3.0:
dependencies:
ansi-styles: 3.2.1
@ -11443,6 +11477,8 @@ snapshots:
code-block-writer@12.0.0: {}
codepage@1.15.0: {}
collect-v8-coverage@1.0.3: {}
color-convert@1.9.3:
@ -11551,6 +11587,8 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
crc-32@1.2.2: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@ -12094,7 +12132,7 @@ snapshots:
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@ -12124,7 +12162,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
@ -12139,7 +12177,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -12460,6 +12498,8 @@ snapshots:
dependencies:
flat-cache: 3.2.0
file-saver@2.0.5: {}
file-selector@2.1.2:
dependencies:
tslib: 2.8.1
@ -12530,6 +12570,8 @@ snapshots:
forwarded@0.2.0: {}
frac@1.1.2: {}
framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
motion-dom: 11.18.1
@ -16050,6 +16092,10 @@ snapshots:
sprintf-js@1.0.3: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
stable-hash@0.0.5: {}
stack-utils@2.0.6:
@ -16867,8 +16913,12 @@ snapshots:
dependencies:
isexe: 2.0.0
wmf@1.0.2: {}
word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
@ -16898,6 +16948,16 @@ snapshots:
ws@8.18.3: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}

View File

@ -45,7 +45,7 @@ export async function getCsrfToken() {
"content-type": "application/json",
};
return httpGet(pathUrl, headers);
// const url = 'https://netidhub.com/api/csrf';
// const url = 'https://new.netidhub.com/api/csrf';
// try {
// const response = await fetch(url, {
// method: 'GET',

View File

@ -1,6 +1,6 @@
import axios from "axios";
const baseURL = "https://netidhub.com/api/";
const baseURL = "https://new.netidhub.com/api/";
const axiosBaseInstance = axios.create({
baseURL,

View File

@ -2,7 +2,7 @@ import axios from "axios";
import Cookies from "js-cookie";
import { getCsrfToken, login } from "../auth";
const baseURL = "https://netidhub.com/api/";
const baseURL = "https://new.netidhub.com/api/";
const refreshToken = Cookies.get("refresh_token");

View File

@ -12,7 +12,7 @@ export async function getCsrfToken() {
"content-type": "application/json",
};
return httpGet(pathUrl, headers);
// const url = 'https://netidhub.com/api/csrf';
// const url = 'https://new.netidhub.com/api/csrf';
// try {
// const response = await fetch(url, {
// method: 'GET',

View File

@ -1,6 +1,8 @@
import api from "@/src/lib/api";
import {
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "../http-config/http-interceptor-service";
export async function getMediaTrackingMonitoring(page: number, size: number) {
@ -67,3 +69,36 @@ export async function listDataAllNonPagination(search: string) {
`media/public/list?enablePage=0&sort=desc&title=${search || ""}`
);
}
export async function validateMediaLink(resultId: number, isRelevant: boolean) {
const url = "media/tracking/monitoring/results/relevant";
const payload = {
resultId,
isRelevant,
};
return httpPutInterceptor(url, payload);
}
// export const validateMediaLink = async (
// resultId: number,
// isRelevant: boolean
// ) => {
// try {
// const res = await api.put(
// "/media/tracking/monitoring/results/relevant",
// {
// resultId,
// isRelevant,
// }
// );
// return res.data;
// } catch (error: any) {
// throw new Error(
// error?.response?.data?.messages?.[0] ||
// "Gagal memperbarui status relevansi"
// );
// }
// };

11
src/lib/api.ts Normal file
View File

@ -0,0 +1,11 @@
import axios from "axios";
const api = axios.create({
baseURL: "https://new.netidhub.com/api",
headers: {
"Content-Type": "application/json",
},
});
export default api;

9
src/types/react-table.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
// src/types/react-table.d.ts
import "@tanstack/react-table";
declare module "@tanstack/react-table" {
interface TableMeta<TData> {
updateData: (rowIndex: number, value: Partial<TData>) => void;
refetchData?: () => void;
}
}

7
types/file-saver.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare module "file-saver" {
export function saveAs(
data: Blob | File | string,
filename?: string,
options?: any
): void;
}

View File

@ -0,0 +1,73 @@
import * as XLSX from "xlsx";
import { getMediaTrackingResult } from "@/service/media-tracking/media-tracking";
import { saveAs } from "file-saver";
type ExportParams = {
mediaTrackingId: number;
};
export async function exportMediaTrackingToExcel({
mediaTrackingId,
}: ExportParams) {
let page = 0;
let totalPages = 1;
const allData: any[] = [];
while (page < totalPages) {
const res = await getMediaTrackingResult({
id: mediaTrackingId,
page,
});
const data = res?.data?.data;
if (!data) break;
totalPages = data.totalPages;
allData.push(...data.content);
page++;
}
if (allData.length === 0) {
throw new Error("Tidak ada data untuk di-export");
}
function extractDomain(url?: string): string {
if (!url) return "-";
try {
return new URL(url).hostname;
} catch {
return "-";
}
}
const rows = allData.map((item, index) => ({
No: index + 1,
"Media Online": extractDomain(item.link),
"Judul Berita": item.title ?? "-",
"Link Berita": item.link ?? "-",
Validasi: item.validationStatus ?? "-",
View: item.viewCount ?? 0,
Share: item.shareCount ?? 0,
Komentar: item.commentCount ?? 0,
Tanggal: item.createdAt
? new Date(item.createdAt).toLocaleString("id-ID")
: "-",
}));
const worksheet = XLSX.utils.json_to_sheet(rows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Media Tracking");
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, `media-tracking-${mediaTrackingId}.xlsx`);
}