This commit is contained in:
hanif salafi 2025-09-03 08:02:19 +07:00
commit e873af1f56
11 changed files with 923 additions and 668 deletions

View File

@ -303,7 +303,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const firstType = typeSplit[0] as EventType; const firstType = typeSplit[0] as EventType;
const colors: Record<EventType, string> = { const colors: Record<EventType, string> = {
"0": "bg-black", "0": "bg-gray-500",
"1": "bg-yellow-500", "1": "bg-yellow-500",
"2": "bg-blue-400", "2": "bg-blue-400",
"3": "bg-slate-400", "3": "bg-slate-400",
@ -408,13 +408,13 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const hasMoreEvents = events.length > 3; const hasMoreEvents = events.length > 3;
return ( return (
<div className="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 pb-3 mr-1"> <div className="flex-1 bg-white dark:bg-black rounded-lg shadow-sm border border-gray-200 pb-3 mr-1">
<div className="py-3"> <div className="py-3">
<h4 className="font-bold text-center">{label}</h4> <h4 className="font-bold text-center">{label}</h4>
</div> </div>
<div className="px-2"> <div className="px-2">
{events.length === 0 ? ( {events.length === 0 ? (
<div className="mt-1 py-2 rounded-lg bg-white border border-black"> <div className="mt-1 py-2 rounded-lg bg-white dark:bg-black border border-black dark:border-gray-500">
<p className="text-center"> <p className="text-center">
{t("no-data-yet", { defaultValue: "No Data Yet" })} {t("no-data-yet", { defaultValue: "No Data Yet" })}
</p> </p>

View File

@ -226,12 +226,12 @@ const LiveReportTable = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="border border-black rounded-md w-full md:w-fit"> <div className="border dark:bg-transparent border-black dark:border dark:border-white rounded-md w-full md:w-fit">
<Select <Select
value={selectedType} value={selectedType}
onValueChange={(value) => setSelectedType(value)} onValueChange={(value) => setSelectedType(value)}
> >
<SelectTrigger className="w-full md:w-[150px] text-black"> <SelectTrigger className="w-full md:w-[150px] text-black dark:text-white">
<SelectValue placeholder="Tipe" /> <SelectValue placeholder="Tipe" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -78,6 +78,9 @@ const TaskTaTable = () => {
pageIndex: 0, pageIndex: 0,
pageSize: Number(showData), pageSize: Number(showData),
}); });
const [activeTab, setActiveTab] = React.useState<"ta" | "daily" | "special">(
"ta"
);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]); const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [dateFilter, setDateFilter] = React.useState(""); const [dateFilter, setDateFilter] = React.useState("");
const [endDate, setEndDate] = React.useState(""); const [endDate, setEndDate] = React.useState("");
@ -126,51 +129,56 @@ const TaskTaTable = () => {
dateFilter, dateFilter,
filterByCode, filterByCode,
statusFilter, statusFilter,
activeTab,
]); ]);
async function fetchData() { async function fetchData() {
const formattedStartDate = dateFilter const formattedStartDate = dateFilter
? format(new Date(dateFilter), "yyyy-MM-dd") ? format(new Date(dateFilter), "yyyy-MM-dd")
: ""; : "";
try { try {
const res = isSpecificAttention let res;
? await listTaskTa(
page - 1, if (activeTab === "ta") {
search, res = await listTaskTa(
showData, page - 1,
filterByCode, search,
formattedStartDate, showData,
"atensi-khusus", filterByCode,
statusFilter formattedStartDate,
) "atensi-khusus",
: await listTask( statusFilter
page - 1, );
search, } else if (activeTab === "daily") {
showData, res = await listTaskTa(
filterByCode, page - 1,
formattedStartDate, search,
"atensi-khusus", showData,
statusFilter filterByCode,
); formattedStartDate,
"tugas-harian",
statusFilter
);
} else if (activeTab === "special") {
res = await listTask(
page - 1,
search,
showData,
filterByCode,
formattedStartDate,
"atensi-khusus",
statusFilter
);
}
const data = res?.data?.data; const data = res?.data?.data;
const contentData = data?.content; const contentData = data?.content || [];
// let contentDataFilter = res?.data?.data?.content || [];
// Filter berdasarkan status
// contentDataFilter = contentDataFilter.filter((item: any) => {
// const isSelesai = statusFilter.includes(1) ? item.isDone : true;
// const isAktif = statusFilter.includes(2) ? item.isActive : true;
// return isSelesai && isAktif;
// });
contentData.forEach((item: any, index: number) => { contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1; item.no = (page - 1) * Number(showData) + index + 1;
}); });
console.log("contentData : ", contentData);
setDataTable(contentData); setDataTable(contentData);
setTotalData(data?.totalElements); setTotalData(data?.totalElements);
setTotalPage(data?.totalPages); setTotalPage(data?.totalPages);
@ -179,6 +187,57 @@ const TaskTaTable = () => {
} }
} }
// async function fetchData() {
// const formattedStartDate = dateFilter
// ? format(new Date(dateFilter), "yyyy-MM-dd")
// : "";
// try {
// const res = isSpecificAttention
// ? await listTaskTa(
// page - 1,
// search,
// showData,
// filterByCode,
// formattedStartDate,
// "atensi-khusus",
// statusFilter
// )
// : await listTask(
// page - 1,
// search,
// showData,
// filterByCode,
// formattedStartDate,
// "atensi-khusus",
// statusFilter
// );
// const data = res?.data?.data;
// const contentData = data?.content;
// // let contentDataFilter = res?.data?.data?.content || [];
// // Filter berdasarkan status
// // contentDataFilter = contentDataFilter.filter((item: any) => {
// // const isSelesai = statusFilter.includes(1) ? item.isDone : true;
// // const isAktif = statusFilter.includes(2) ? item.isActive : true;
// // return isSelesai && isAktif;
// // });
// contentData.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// console.log("contentData : ", contentData);
// setDataTable(contentData);
// setTotalData(data?.totalElements);
// setTotalPage(data?.totalPages);
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterByCode(e.target.value); setFilterByCode(e.target.value);
setSearch(e.target.value); setSearch(e.target.value);
@ -212,7 +271,44 @@ const TaskTaTable = () => {
onChange={() => setIsSpecificAttention(!isSpecificAttention)} onChange={() => setIsSpecificAttention(!isSpecificAttention)}
hidden hidden
/> />
<span <div className="flex mb-6">
<button
onClick={() => setActiveTab("ta")}
className={`px-4 py-1 rounded transition ${
activeTab === "ta"
? "bg-default-900 text-white"
: "border dark:text-default-700"
}`}
>
Atensi Khusus TA
</button>
<button
onClick={() => setActiveTab("daily")}
className={`px-4 py-1 rounded transition ${
activeTab === "daily"
? "bg-default-900 text-white"
: "border dark:text-default-700"
}`}
>
{t("daily-tasks", { defaultValue: "Daily Tasks" })}
</button>
<button
onClick={() => setActiveTab("special")}
className={`px-4 py-1 rounded transition ${
activeTab === "special"
? "bg-default-900 text-white"
: "border dark:text-default-700"
}`}
>
{t("special-attention", {
defaultValue: "Special Attention",
})}
</button>
</div>
{/* <span
className={` ${ className={` ${
isSpecificAttention isSpecificAttention
? "bg-default-900 text-white" ? "bg-default-900 text-white"
@ -224,6 +320,18 @@ const TaskTaTable = () => {
</span> </span>
<span <span
className={` className={`
${
!isSpecificAttention
? "bg-default-900 text-white dark:text-black"
: " dark:text-default-700 border-2 dark:border dark:border-gray-500"
}
px-[18px] py-1 transition duration-100 rounded
`}
>
{t("daily-tasks", { defaultValue: "Daily Tasks" })}
</span>
<span
className={`
${ ${
!isSpecificAttention !isSpecificAttention
? "bg-default-900 text-white" ? "bg-default-900 text-white"
@ -235,7 +343,7 @@ const TaskTaTable = () => {
{t("special-attention", { {t("special-attention", {
defaultValue: "Special Attention", defaultValue: "Special Attention",
})}{" "} })}{" "}
</span> </span> */}
</label> </label>
</div> </div>
</div> </div>

View File

@ -207,7 +207,7 @@ const TaskTable = () => {
className={` ${ className={` ${
isSpecificAttention isSpecificAttention
? "bg-default-900 text-white dark:text-black" ? "bg-default-900 text-white dark:text-black"
: "dark:text-default-700 border-2" : "dark:text-default-700 border-2 dark:border dark:border-gray-500"
} }
px-[18px] py-1 transition duration-100 rounded`} px-[18px] py-1 transition duration-100 rounded`}
> >
@ -218,7 +218,7 @@ const TaskTable = () => {
${ ${
!isSpecificAttention !isSpecificAttention
? "bg-default-900 text-white dark:text-black" ? "bg-default-900 text-white dark:text-black"
: " dark:text-default-700 border-2" : " dark:text-default-700 border-2 dark:border dark:border-gray-500"
} }
px-[18px] py-1 transition duration-100 rounded px-[18px] py-1 transition duration-100 rounded
`} `}

View File

@ -18,7 +18,7 @@ const CommunicationPage = () => {
return ( return (
<div> <div>
<SiteBreadcrumb /> <SiteBreadcrumb />
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3"> <div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between py-3"> <div className="flex justify-between py-3">
<p className="text-lg">{tab}</p> <p className="text-lg">{tab}</p>
{tab === "Pertanyaan Internal" && ( {tab === "Pertanyaan Internal" && (

View File

@ -5,7 +5,7 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "ckeditor5-custom-build"; import Editor from "ckeditor5-custom-build";
function CustomEditor(props) { function CustomEditor(props) {
const maxHeight = props.maxHeight || 600; // Default max height 600px const maxHeight = props.maxHeight || 600;
return ( return (
<div className="ckeditor-wrapper"> <div className="ckeditor-wrapper">

View File

@ -4,7 +4,7 @@ import Editor from "ckeditor5-custom-build";
function ViewEditor(props) { function ViewEditor(props) {
const maxHeight = props.maxHeight || 600; // Default max height 600px const maxHeight = props.maxHeight || 600; // Default max height 600px
return ( return (
<div className="ckeditor-view-wrapper"> <div className="ckeditor-view-wrapper">
<CKEditor <CKEditor
@ -12,15 +12,14 @@ function ViewEditor(props) {
data={props.initialData} data={props.initialData}
disabled={true} disabled={true}
config={{ config={{
// toolbar: [],
isReadOnly: true, isReadOnly: true,
// Add content styling configuration for read-only mode
content_style: ` content_style: `
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #333; color: #111;
background: #fff;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@ -41,66 +40,105 @@ function ViewEditor(props) {
background-color: #f9fafb; background-color: #f9fafb;
} }
`, `,
// Editor appearance settings
height: props.height || 400, height: props.height || 400,
removePlugins: ['Title'], removePlugins: ["Title"],
}} }}
/> />
<style jsx>{` <style jsx>{`
.ckeditor-view-wrapper { .ckeditor-view-wrapper {
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
} }
.ckeditor-view-wrapper :global(.ck.ck-editor__main) { .ckeditor-view-wrapper :global(.ck.ck-editor__main) {
min-height: ${props.height || 400}px; min-height: ${props.height || 400}px;
max-height: ${maxHeight}px; max-height: ${maxHeight}px;
} }
.ckeditor-view-wrapper :global(.ck.ck-editor__editable) { .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
min-height: ${(props.height || 400) - 50}px; min-height: ${(props.height || 400) - 50}px;
max-height: ${maxHeight - 50}px; max-height: ${maxHeight - 50}px;
overflow-y: auto !important; overflow-y: auto !important;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9; scrollbar-color: #cbd5e1 #f1f5f9;
background-color:rgb(253, 253, 253); background-color: #fdfdfd;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 6px; border-radius: 6px;
color: #111;
} }
/* Custom scrollbar styling for webkit browsers */ /* 🌙 Dark mode support */
.ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) { :global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
background-color: #111 !important;
color: #f9fafb !important;
border-color: #374151;
}
:global(.dark) .ckeditor-view-wrapper h1,
:global(.dark) .ckeditor-view-wrapper h2,
:global(.dark) .ckeditor-view-wrapper h3,
:global(.dark) .ckeditor-view-wrapper h4,
:global(.dark) .ckeditor-view-wrapper h5,
:global(.dark) .ckeditor-view-wrapper h6 {
color: #f9fafb !important;
}
:global(.dark) .ckeditor-view-wrapper blockquote {
background-color: #1f2937 !important;
border-left: 4px solid #374151 !important;
color: #f3f4f6 !important;
}
/* Custom scrollbar styling */
.ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
width: 8px; width: 8px;
} }
.ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) { .ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #f1f5f9; background: #f1f5f9;
border-radius: 4px; border-radius: 4px;
} }
.ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) { .ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #cbd5e1; background: #cbd5e1;
border-radius: 4px; border-radius: 4px;
} }
.ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) { .ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #94a3b8; background: #94a3b8;
} }
/* Ensure content doesn't overflow */ /* 🌙 Dark mode scrollbar */
.ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) { :global(.dark)
overflow: hidden; .ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #1f2937;
} }
:global(.dark)
.ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #4b5563;
}
:global(.dark)
.ckeditor-view-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #6b7280;
}
/* Read-only specific styling */ /* Read-only specific styling */
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) { .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
background-color: #f8fafc;
color: #4b5563;
cursor: default; cursor: default;
} }
/* Hide toolbar for view-only mode */ /* Hide toolbar */
.ckeditor-view-wrapper :global(.ck.ck-toolbar) { .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
display: none !important; display: none !important;
} }
@ -110,3 +148,116 @@ function ViewEditor(props) {
} }
export default ViewEditor; export default ViewEditor;
// import React from "react";
// import { CKEditor } from "@ckeditor/ckeditor5-react";
// import Editor from "ckeditor5-custom-build";
// function ViewEditor(props) {
// const maxHeight = props.maxHeight || 600;
// return (
// <div className="ckeditor-view-wrapper">
// <CKEditor
// editor={Editor}
// data={props.initialData}
// disabled={true}
// config={{
// // toolbar: [],
// isReadOnly: true,
// // Add content styling configuration for read-only mode
// content_style: `
// body {
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
// font-size: 14px;
// line-height: 1.6;
// color: #333;
// margin: 0;
// padding: 0;
// }
// p {
// margin: 0.5em 0;
// }
// h1, h2, h3, h4, h5, h6 {
// margin: 1em 0 0.5em 0;
// }
// ul, ol {
// margin: 0.5em 0;
// padding-left: 2em;
// }
// blockquote {
// margin: 1em 0;
// padding: 0.5em 1em;
// border-left: 4px solid #d1d5db;
// background-color: #f9fafb;
// }
// `,
// // Editor appearance settings
// height: props.height || 400,
// removePlugins: ['Title'],
// }}
// />
// <style jsx>{`
// .ckeditor-view-wrapper {
// border-radius: 6px;
// overflow: hidden;
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
// }
// .ckeditor-view-wrapper :global(.ck.ck-editor__main) {
// min-height: ${props.height || 400}px;
// max-height: ${maxHeight}px;
// }
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
// min-height: ${(props.height || 400) - 50}px;
// max-height: ${maxHeight - 50}px;
// overflow-y: auto !important;
// scrollbar-width: thin;
// scrollbar-color: #cbd5e1 #f1f5f9;
// background-color:rgb(253, 253, 253);
// border: 1px solid #d1d5db;
// border-radius: 6px;
// }
// /* Custom scrollbar styling for webkit browsers */
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
// width: 8px;
// }
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
// background: #f1f5f9;
// border-radius: 4px;
// }
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
// background: #cbd5e1;
// border-radius: 4px;
// }
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
// background: #94a3b8;
// }
// /* Ensure content doesn't overflow */
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) {
// overflow: hidden;
// }
// /* Read-only specific styling */
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
// background-color: #f8fafc;
// color: #4b5563;
// cursor: default;
// }
// /* Hide toolbar for view-only mode */
// .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
// display: none !important;
// }
// `}</style>
// </div>
// );
// }
// export default ViewEditor;

View File

@ -729,7 +729,7 @@ export default function FormConvertSPIT() {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */} {/* Main Content */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-3 space-y-6">
{/* Basic Information */} {/* Basic Information */}
<Card> <Card>
<CardHeader> <CardHeader>
@ -797,138 +797,40 @@ export default function FormConvertSPIT() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> <CardContent className="space-y-8">
{/* Original Content */} {/* Pilih Upload Type */}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-lg text-black">Original Content</Label> <Label>Upload Type</Label>
<Controller <RadioGroup
control={control} value={selectedFileType}
name="contentDescription" onValueChange={(value: "original" | "rewrite") =>
render={({ field }) => ( setSelectedFileType(value)
<CustomEditor }
onChange={field.onChange} className="grid grid-cols-2 gap-4"
initialData={field.value} >
/> <div className="flex items-center space-x-2">
)} <RadioGroupItem value="original" id="original" />
/> <Label htmlFor="original">Original Content</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="rewrite" id="rewrite" />
<Label htmlFor="rewrite">Rewritten Content</Label>
</div>
</RadioGroup>
</div> </div>
{/* Content Rewrite */} {/* Tampilkan keduanya berdampingan */}
<div className="space-y-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Label className="text-lg text-black"> {/* Original Content */}
Rewritten Content <div
</Label> className={`space-y-2 p-4 rounded-lg border ${
selectedFileType === "original"
<div className="flex items-center justify-between"> ? "border-blue-600"
<div className="space-y-2"> : "border-gray-300"
<Label>Writing Style</Label> }`}
<Select >
value={selectedWritingStyle} <Label className="text-lg text-black">
onValueChange={setSelectedWritingStyle} Original Content
> </Label>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select style" />
</SelectTrigger>
<SelectContent>
{WRITING_STYLES.map((style) => (
<SelectItem key={style.value} value={style.value}>
{style.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
type="button"
onClick={handleRewriteClick}
disabled={
isGeneratingRewrite || !detail?.contentDescription
}
className="bg-blue-600 hover:bg-blue-700"
>
{isGeneratingRewrite ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Edit3 className="h-4 w-4 mr-2" />
)}
Generate Rewrite
</Button>
</div>
{showRewriteEditor && (
<div className="space-y-4">
{articleIds.length > 0 && (
<div className="flex gap-2">
{articleIds.map((articleId, index) => (
<Button
key={articleId}
type="button"
variant={
selectedArticleId === articleId
? "default"
: "outline"
}
size="sm"
onClick={() => handleArticleSelect(articleId)}
disabled={isLoadingRewrite}
>
{isLoadingRewrite &&
selectedArticleId === articleId && (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
)}
Narrative {index + 1}
</Button>
))}
</div>
)}
<div className="space-y-2">
<Label>Rewritten Content</Label>
<Controller
control={control}
name="contentRewriteDescription"
render={({ field }) => (
<CustomEditor
onChange={field.onChange}
initialData={articleBody || field.value}
/>
)}
/>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* <Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Edit3 className="h-5 w-5" />
Content Editor
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<RadioGroup
value={selectedFileType}
onValueChange={(value: "original" | "rewrite") =>
setSelectedFileType(value)
}
className="grid grid-cols-2 gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="original" id="original" />
<Label htmlFor="original">Original Content</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="rewrite" id="rewrite" />
<Label htmlFor="rewrite">Rewritten Content</Label>
</div>
</RadioGroup>
{/* Original Content */}
{/* {selectedFileType === "original" && (
<div className="space-y-2">
<Label>Content Description</Label>
<Controller <Controller
control={control} control={control}
name="contentDescription" name="contentDescription"
@ -940,11 +842,19 @@ export default function FormConvertSPIT() {
)} )}
/> />
</div> </div>
)} */}
{/* Content Rewrite */} {/* Rewrite Content */}
{/* {selectedFileType === "rewrite" && ( <div
<div className="space-y-4"> className={`space-y-4 p-4 rounded-lg border ${
selectedFileType === "rewrite"
? "border-blue-600"
: "border-gray-300"
}`}
>
<Label className="text-lg text-black">
Rewritten Content
</Label>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-2"> <div className="space-y-2">
<Label>Writing Style</Label> <Label>Writing Style</Label>
@ -953,7 +863,7 @@ export default function FormConvertSPIT() {
onValueChange={setSelectedWritingStyle} onValueChange={setSelectedWritingStyle}
> >
<SelectTrigger className="w-48"> <SelectTrigger className="w-48">
<SelectValue /> <SelectValue placeholder="Select style" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{WRITING_STYLES.map((style) => ( {WRITING_STYLES.map((style) => (
@ -1024,359 +934,367 @@ export default function FormConvertSPIT() {
</div> </div>
)} )}
</div> </div>
)} </div>
</CardContent> </CardContent>
</Card> */} </Card>
{/* Media Files */} {/* Media Files */}
{detailThumb.length > 0 && ( <div className="flex flex-col lg:flex-row gap-6">
<Card> <div className="flex-1 space-y-6">
<CardHeader> {detailThumb.length > 0 && (
<CardTitle className="flex items-center gap-2"> <Card>
<Image className="h-5 w-5" /> <CardHeader>
Media Files <CardTitle className="flex items-center gap-2">
</CardTitle> <Image className="h-5 w-5" />
</CardHeader> Media Files
<CardContent className="space-y-4"> </CardTitle>
<div className="space-y-4"> </CardHeader>
<Swiper <CardContent className="space-y-4">
thumbs={{ swiper: thumbsSwiper }} <div className="space-y-4">
modules={[FreeMode, Navigation, Thumbs]} <Swiper
navigation={true} thumbs={{ swiper: thumbsSwiper }}
className="w-full h-96" modules={[FreeMode, Navigation, Thumbs]}
> navigation={true}
{detailThumb.map((item) => ( className="w-full h-96"
<SwiperSlide key={item.contentId}> >
{item.contentType === "VIDEO" ? ( {detailThumb.map((item) => (
<div className="relative max-h-screen overflow-hidden"> <SwiperSlide key={item.contentId}>
<div className="w-full max-h-screen aspect-video"> {item.contentType === "VIDEO" ? (
<div className="w-full h-full object-contain"> <div className="relative max-h-screen overflow-hidden">
{/* main video player */} <div className="w-full max-h-screen aspect-video">
<video <div className="w-full h-full object-contain">
className="object-contain h-full w-full rounded-lg" {/* main video player */}
src={item.contentFile} <video
controls className="object-contain h-full w-full rounded-lg"
// playsInline to better on mobile src={item.contentFile}
playsInline controls
// you can set poster if available: poster={item.thumbnailFileUrl} // playsInline to better on mobile
title={`Video ${item.contentId}`} playsInline
/> // you can set poster if available: poster={item.thumbnailFileUrl}
title={`Video ${item.contentId}`}
/>
</div>
</div>
</div> </div>
</div> ) : (
</div> <img
) : ( src={item.contentFile}
<img alt={`Media ${item.contentId}`}
src={item.contentFile} className="w-full h-full object-cover rounded-lg"
alt={`Media ${item.contentId}`} />
className="w-full h-full object-cover rounded-lg" )}
/> </SwiperSlide>
)} ))}
</SwiperSlide> </Swiper>
))}
</Swiper>
<Swiper <Swiper
onSwiper={setThumbsSwiper} onSwiper={setThumbsSwiper}
slidesPerView={8} slidesPerView={8}
spaceBetween={8} spaceBetween={8}
modules={[Pagination, Thumbs]} modules={[Pagination, Thumbs]}
className="w-full" className="w-full"
> >
{detailThumb.map((item) => ( {detailThumb.map((item) => (
<SwiperSlide key={`thumb-${item.contentId}`}> <SwiperSlide key={`thumb-${item.contentId}`}>
{item.contentType === "VIDEO" ? ( {item.contentType === "VIDEO" ? (
<div className="relative w-full h-16 rounded cursor-pointer overflow-hidden"> <div className="relative w-full h-16 rounded cursor-pointer overflow-hidden">
{/* use preload metadata so browser doesn't download full video */} {/* use preload metadata so browser doesn't download full video */}
<video
src={item.contentFile}
className="w-full h-16 object-cover"
muted
preload="metadata"
playsInline
// no controls in thumbnail
tabIndex={-1}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/30 pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
) : (
<img
src={item.contentFile}
alt={`Thumbnail ${item.contentId}`}
className="w-full h-16 object-cover rounded cursor-pointer"
/>
)}
</SwiperSlide>
))}
</Swiper>
</div>
</CardContent>
</Card>
)}
{/* File Placement */}
{files.length > 0 && isUserMabesApprover && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" />
File Placement
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{files.length > 1 && (
<div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg">
{PLACEMENT_OPTIONS.map((option) => (
<div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
id={`select-all-${option.value}`}
onCheckedChange={(checked) =>
handleSelectAllPlacements(
option.value,
Boolean(checked)
)
}
/>
<Label
htmlFor={`select-all-${option.value}`}
className="text-sm"
>
All {option.label}
</Label>
</div>
))}
</div>
)}
<div className="space-y-4">
{files.map((file, index) => (
<div
key={file.contentId}
className="flex gap-4 p-4 border rounded-lg"
>
{/* show thumbnail or video preview */}
{file.contentType === "VIDEO" ? (
<video <video
src={item.contentFile} src={file.contentFile}
className="w-full h-16 object-cover" className="w-32 h-24 object-cover rounded"
muted muted
preload="metadata" preload="metadata"
playsInline playsInline
// no controls in thumbnail
tabIndex={-1}
/> />
<div className="absolute inset-0 flex items-center justify-center bg-black/30 pointer-events-none"> ) : (
<svg <img
xmlns="http://www.w3.org/2000/svg" src={file.contentFile}
className="w-6 h-6 text-white" alt={file.fileName || `file-${file.contentId}`}
fill="currentColor" className="w-32 h-24 object-cover rounded"
viewBox="0 0 24 24" />
> )}
<path d="M8 5v14l11-7z" />
</svg> <div className="flex-1 space-y-3">
<p className="font-medium text-sm">
{file.fileName ||
file.contentFileName ||
`File ${file.contentId}`}
</p>
<div className="flex flex-wrap gap-3">
{PLACEMENT_OPTIONS.map((option) => (
<div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
id={`${file.contentId}-${option.value}`}
checked={filePlacements[index]?.includes(
option.value
)}
onCheckedChange={(checked) =>
handleFilePlacementChange(
index,
option.value,
Boolean(checked)
)
}
/>
<Label
htmlFor={`${file.contentId}-${option.value}`}
className="text-sm"
>
{option.label}
</Label>
</div>
))}
</div> </div>
</div> </div>
) : ( </div>
<img ))}
src={item.contentFile} </div>
alt={`Thumbnail ${item.contentId}`} </CardContent>
className="w-full h-16 object-cover rounded cursor-pointer" </Card>
/> )}
)} </div>
</SwiperSlide>
))}
</Swiper>
</div>
</CardContent>
</Card>
)}
{/* File Placement */} {/* Sidebar */}
{files.length > 0 && isUserMabesApprover && ( <div className="w-full lg:w-[30%] space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" /> <Users className="h-5 w-5" />
File Placement Creator Information
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{files.length > 1 && ( <div className="space-y-2">
<div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg"> <Label htmlFor="creator">Creator *</Label>
{PLACEMENT_OPTIONS.map((option) => ( <Controller
<div control={control}
key={option.value} name="contentCreator"
className="flex items-center space-x-2" render={({ field }) => (
> <Input
<Checkbox id="creator"
id={`select-all-${option.value}`} placeholder="Enter creator name"
onCheckedChange={(checked) => {...field}
handleSelectAllPlacements(
option.value,
Boolean(checked)
)
}
/>
<Label
htmlFor={`select-all-${option.value}`}
className="text-sm"
>
All {option.label}
</Label>
</div>
))}
</div>
)}
<div className="space-y-4">
{files.map((file, index) => (
<div
key={file.contentId}
className="flex gap-4 p-4 border rounded-lg"
>
{/* show thumbnail or video preview */}
{file.contentType === "VIDEO" ? (
<video
src={file.contentFile}
className="w-32 h-24 object-cover rounded"
muted
preload="metadata"
playsInline
/>
) : (
<img
src={file.contentFile}
alt={file.fileName || `file-${file.contentId}`}
className="w-32 h-24 object-cover rounded"
/> />
)} )}
/>
{errors.contentCreator && (
<Alert variant="soft">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{errors.contentCreator.message}
</AlertDescription>
</Alert>
)}
</div>
</CardContent>
</Card>
<div className="flex-1 space-y-3"> {/* Preview */}
<p className="font-medium text-sm"> {detail?.contentThumbnail && (
{file.fileName || <Card>
file.contentFileName || <CardHeader>
`File ${file.contentId}`} <CardTitle className="flex items-center gap-2">
</p> <Eye className="h-5 w-5" />
<div className="flex flex-wrap gap-3"> Preview
{PLACEMENT_OPTIONS.map((option) => ( </CardTitle>
<div </CardHeader>
key={option.value} <CardContent>
className="flex items-center space-x-2" <img
> src={detail.contentThumbnail}
<Checkbox alt="Content thumbnail"
id={`${file.contentId}-${option.value}`} className="w-full h-auto rounded-lg"
checked={filePlacements[index]?.includes( />
option.value </CardContent>
)} </Card>
onCheckedChange={(checked) => )}
handleFilePlacementChange(
index, {/* Tags */}
option.value, <Card>
Boolean(checked) <CardHeader>
) <CardTitle className="flex items-center gap-2">
} <Tag className="h-5 w-5" />
/> Tags
<Label </CardTitle>
htmlFor={`${file.contentId}-${option.value}`} </CardHeader>
className="text-sm" <CardContent className="space-y-4">
> <div className="space-y-2">
{option.label} <Label htmlFor="tag-input">Add Tags</Label>
</Label> <Input
</div> id="tag-input"
))} placeholder="Type a tag and press Enter"
</div> onKeyDown={handleAddTag}
</div> ref={inputRef}
/>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge
key={index}
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
onClick={() => handleRemoveTag(index)}
>
{tag}
<XCircle className="h-3 w-3 ml-1" />
</Badge>
))}
</div>
)}
</CardContent>
</Card>
{/* Publish Targets */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Publish Targets
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{PUBLISH_OPTIONS.map((option) => (
<div
key={option.id}
className="flex items-center space-x-2"
>
<Checkbox
id={option.id}
checked={
option.id === "all"
? publishedFor.length ===
PUBLISH_OPTIONS.filter(
(opt) => opt.id !== "all"
).length
: publishedFor.includes(option.id)
}
onCheckedChange={() =>
handlePublishTargetChange(option.id)
}
/>
<Label htmlFor={option.id} className="text-sm">
{option.label}
</Label>
</div> </div>
))} ))}
</div> </CardContent>
</CardContent> </Card>
</Card>
)}
</div>
{/* Sidebar */} {/* Submit Button */}
<div className="space-y-6"> <Card>
{/* Creator Information */} <CardContent className="pt-6">
<Card> <Button
<CardHeader> type="submit"
<CardTitle className="flex items-center gap-2"> className="w-full mb-4"
<Users className="h-5 w-5" /> disabled={isSubmitting || isSaving || isAlreadySaved}
Creator Information >
</CardTitle> {isSubmitting || isSaving ? (
</CardHeader> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
<CardContent className="space-y-4"> ) : (
<div className="space-y-2"> <Save className="h-4 w-4 mr-2" />
<Label htmlFor="creator">Creator *</Label> )}
<Controller {isAlreadySaved
control={control} ? "Already Saved"
name="contentCreator" : isSubmitting || isSaving
render={({ field }) => ( ? "Saving..."
<Input : "Save Changes"}
id="creator" </Button>
placeholder="Enter creator name"
{...field} {isAlreadySaved && (
/> <Alert variant="soft">
<CheckCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-500">
Konten sudah disimpan. Anda tidak dapat menyimpan
ulang.
</AlertDescription>
</Alert>
)} )}
/> </CardContent>
{errors.contentCreator && ( </Card>
<Alert variant="soft"> </div>
<AlertCircle className="h-4 w-4" /> </div>
<AlertDescription>
{errors.contentCreator.message}
</AlertDescription>
</Alert>
)}
</div>
</CardContent>
</Card>
{/* Preview */}
{detail?.contentThumbnail && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Eye className="h-5 w-5" />
Preview
</CardTitle>
</CardHeader>
<CardContent>
<img
src={detail.contentThumbnail}
alt="Content thumbnail"
className="w-full h-auto rounded-lg"
/>
</CardContent>
</Card>
)}
{/* Tags */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
Tags
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tag-input">Add Tags</Label>
<Input
id="tag-input"
placeholder="Type a tag and press Enter"
onKeyDown={handleAddTag}
ref={inputRef}
/>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge
key={index}
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
onClick={() => handleRemoveTag(index)}
>
{tag}
<XCircle className="h-3 w-3 ml-1" />
</Badge>
))}
</div>
)}
</CardContent>
</Card>
{/* Publish Targets */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Publish Targets
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{PUBLISH_OPTIONS.map((option) => (
<div key={option.id} className="flex items-center space-x-2">
<Checkbox
id={option.id}
checked={
option.id === "all"
? publishedFor.length ===
PUBLISH_OPTIONS.filter((opt) => opt.id !== "all")
.length
: publishedFor.includes(option.id)
}
onCheckedChange={() =>
handlePublishTargetChange(option.id)
}
/>
<Label htmlFor={option.id} className="text-sm">
{option.label}
</Label>
</div>
))}
</CardContent>
</Card>
{/* Submit Button */}
<Card>
<CardContent className="pt-6">
<Button
type="submit"
className="w-full mb-4"
disabled={isSubmitting || isSaving || isAlreadySaved}
>
{isSubmitting || isSaving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
{isAlreadySaved
? "Already Saved"
: isSubmitting || isSaving
? "Saving..."
: "Save Changes"}
</Button>
{isAlreadySaved && (
<Alert variant="soft">
<CheckCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-500">
Konten sudah disimpan. Anda tidak dapat menyimpan ulang.
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div> </div>
</div> </div>
</form> </form>

View File

@ -87,13 +87,13 @@ export default function PublishMediahub() {
const t = useTranslations("Form"); const t = useTranslations("Form");
const [mainType, setMainType] = useState<number>(1); const [mainType, setMainType] = useState<number>(1);
const [taskType, setTaskType] = useState<string>("atensi-khusus"); const [taskType, setTaskType] = useState<string>("atensi-khusus");
const [broadcastType, setBroadcastType] = useState<string>("all"); // untuk Tipe Penugasan const [broadcastType, setBroadcastType] = useState<string>("all");
const [type, setType] = useState<string>("1"); const [type, setType] = useState<string>("1");
const [selectedTarget, setSelectedTarget] = useState("all"); const [selectedTarget, setSelectedTarget] = useState("all");
const [startDate, setStartDate] = useState<Date>(new Date()); const [startDate, setStartDate] = useState<Date>(new Date());
const [detail, setDetail] = useState<mediahubDetail>(); const [detail, setDetail] = useState<mediahubDetail>();
const [refresh] = useState(false); const [refresh] = useState(false);
const [listDest, setListDest] = useState([]); // Data Polda dan Polres const [listDest, setListDest] = useState([]);
const [checkedLevels, setCheckedLevels] = useState(new Set()); const [checkedLevels, setCheckedLevels] = useState(new Set());
const [expandedPolda, setExpandedPolda] = useState([{}]); const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -267,7 +267,7 @@ export default function PublishMediahub() {
}; };
if (id) { if (id) {
requestData.id = parseInt(id, 10); // Ensure id is a number requestData.id = parseInt(id, 10);
} }
console.log("Form Data Submitted:", requestData); console.log("Form Data Submitted:", requestData);
@ -491,7 +491,7 @@ export default function PublishMediahub() {
variant="outline" variant="outline"
size="md" size="md"
className={cn( className={cn(
" justify-between text-left font-normal border-default-200 text-default-600 md:px-4 w-3/12", " justify-between text-left font-normal border-default-200 text-default-600 md:px-4 w-3/12 dark:border dark:border-gray-500",
!startDate && "text-muted-foreground" !startDate && "text-muted-foreground"
)} )}
> >

View File

@ -994,7 +994,7 @@ export default function FormTaskEdit() {
<div> <div>
<div <div
key={index} key={index}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md" className=" flex justify-between border px-3.5 py-3 my-6 rounded-md dark:border dark:border-gray-500"
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
@ -1034,7 +1034,7 @@ export default function FormTaskEdit() {
<div> <div>
<div <div
key={index} key={index}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md" className=" flex justify-between border px-3.5 py-3 my-6 rounded-md dark:border dark:border-gray-500"
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">

View File

@ -42,6 +42,7 @@ import { loading } from "@/lib/swal";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import UpdateSection from "@/app/[locale]/(public)/inbox/update/page"; import UpdateSection from "@/app/[locale]/(public)/inbox/update/page";
import { getCookiesDecrypt } from "@/lib/utils";
const taskSchema = z.object({ const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
@ -98,12 +99,11 @@ export default function FormTask() {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter(); const router = useRouter();
const editor = useRef(null); const editor = useRef(null);
const levelNumber = Number(getCookiesDecrypt("ulne")) || 0;
type TaskSchema = z.infer<typeof taskSchema>; type TaskSchema = z.infer<typeof taskSchema>;
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
console.log(id); console.log(id);
const [listDest, setListDest] = useState<Destination[]>([]); const [listDest, setListDest] = useState<Destination[]>([]);
// State for various form fields // State for various form fields
const [taskOutput, setTaskOutput] = useState({ const [taskOutput, setTaskOutput] = useState({
all: false, all: false,
@ -146,12 +146,14 @@ export default function FormTask() {
polres: false, polres: false,
satker: false, satker: false,
}); });
// State untuk melacak apakah perubahan berasal dari checkbox Penerima Tugas // State untuk melacak apakah perubahan berasal dari checkbox Penerima Tugas
const [isUpdatingFromPenerimaTugas, setIsUpdatingFromPenerimaTugas] = useState(false); const [isUpdatingFromPenerimaTugas, setIsUpdatingFromPenerimaTugas] =
useState(false);
// State untuk melacak jenis perubahan spesifik // State untuk melacak jenis perubahan spesifik
const [penerimaTugasChangeType, setPenerimaTugasChangeType] = useState<string>(""); const [penerimaTugasChangeType, setPenerimaTugasChangeType] =
useState<string>("");
const [links, setLinks] = useState<string[]>([""]); const [links, setLinks] = useState<string[]>([""]);
const { const {
register, register,
@ -201,12 +203,14 @@ export default function FormTask() {
setCheckedLevels((prev) => { setCheckedLevels((prev) => {
const updatedLevels = new Set(prev); const updatedLevels = new Set(prev);
const isCurrentlyChecked = updatedLevels.has(levelId); const isCurrentlyChecked = updatedLevels.has(levelId);
if (isCurrentlyChecked) { if (isCurrentlyChecked) {
updatedLevels.delete(levelId); updatedLevels.delete(levelId);
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya // Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find((item: any) => Number(item.id) === levelId); const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
);
if (poldaItem && poldaItem.subDestination) { if (poldaItem && poldaItem.subDestination) {
poldaItem.subDestination.forEach((polres: any) => { poldaItem.subDestination.forEach((polres: any) => {
updatedLevels.delete(Number(polres.id)); updatedLevels.delete(Number(polres.id));
@ -217,7 +221,7 @@ export default function FormTask() {
} }
return updatedLevels; return updatedLevels;
}); });
// Update unitSelection berdasarkan perubahan di modal // Update unitSelection berdasarkan perubahan di modal
updateUnitSelectionFromModal(); updateUnitSelectionFromModal();
}; };
@ -233,7 +237,7 @@ export default function FormTask() {
// Set flag bahwa perubahan berasal dari checkbox Penerima Tugas // Set flag bahwa perubahan berasal dari checkbox Penerima Tugas
setIsUpdatingFromPenerimaTugas(true); setIsUpdatingFromPenerimaTugas(true);
setPenerimaTugasChangeType(key + (value ? "_checked" : "_unchecked")); setPenerimaTugasChangeType(key + (value ? "_checked" : "_unchecked"));
if (key === "allUnit") { if (key === "allUnit") {
const newState = { const newState = {
allUnit: value, allUnit: value,
@ -247,19 +251,22 @@ export default function FormTask() {
// Validasi khusus untuk POLRES // Validasi khusus untuk POLRES
if (key === "polres" && value) { if (key === "polres" && value) {
// Cek apakah ada POLDA yang sudah dichecklist di modal // Cek apakah ada POLDA yang sudah dichecklist di modal
const hasCheckedPolda = listDest.some((item: any) => const hasCheckedPolda = listDest.some(
item.levelNumber === 2 && (item: any) =>
item.name !== "SATKER POLRI" && item.levelNumber === 2 &&
checkedLevels.has(Number(item.id)) item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
); );
if (!hasCheckedPolda) { if (!hasCheckedPolda) {
// Jika tidak ada POLDA yang dichecklist di modal, tampilkan peringatan dan batalkan // Jika tidak ada POLDA yang dichecklist di modal, tampilkan peringatan dan batalkan
alert("Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."); alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
);
return; // Batalkan perubahan return; // Batalkan perubahan
} }
} }
const updatedSelection = { const updatedSelection = {
...unitSelection, ...unitSelection,
[key]: value, [key]: value,
@ -396,52 +403,67 @@ export default function FormTask() {
const updateUnitSelectionFromModal = () => { const updateUnitSelectionFromModal = () => {
setTimeout(() => { setTimeout(() => {
// Hitung total item yang tersedia untuk setiap kategori // Hitung total item yang tersedia untuk setiap kategori
const totalPolda = listDest.filter((item: any) => const totalPolda = listDest.filter(
item.levelNumber === 2 && item.name !== "SATKER POLRI" (item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI"
).length; ).length;
const totalPolres = listDest.reduce((total: number, item: any) => { const totalPolres = listDest.reduce((total: number, item: any) => {
if (item.subDestination) { if (item.subDestination) {
return total + item.subDestination.length; return total + item.subDestination.length;
} }
return total; return total;
}, 0); }, 0);
const satkerItem = listDest.find((item: any) => item.name === "SATKER POLRI"); const satkerItem = listDest.find(
const totalSatker = satkerItem ? (1 + (satkerItem.subDestination?.length || 0)) : 0; (item: any) => item.name === "SATKER POLRI"
);
const totalSatker = satkerItem
? 1 + (satkerItem.subDestination?.length || 0)
: 0;
// Hitung item yang dichecklist untuk setiap kategori // Hitung item yang dichecklist untuk setiap kategori
const checkedPoldaCount = listDest.filter((item: any) => const checkedPoldaCount = listDest.filter(
item.levelNumber === 2 && (item: any) =>
item.name !== "SATKER POLRI" && item.levelNumber === 2 &&
checkedLevels.has(Number(item.id)) item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
).length; ).length;
const checkedPolresCount = listDest.reduce((total: number, item: any) => { const checkedPolresCount = listDest.reduce((total: number, item: any) => {
if (item.subDestination) { if (item.subDestination) {
return total + item.subDestination.filter((sub: any) => checkedLevels.has(Number(sub.id))).length; return (
total +
item.subDestination.filter((sub: any) =>
checkedLevels.has(Number(sub.id))
).length
);
} }
return total; return total;
}, 0); }, 0);
const checkedSatkerCount = satkerItem ? ( const checkedSatkerCount = satkerItem
(checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) + ? (checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) +
(satkerItem.subDestination?.filter((sub: any) => checkedLevels.has(Number(sub.id))).length || 0) (satkerItem.subDestination?.filter((sub: any) =>
) : 0; checkedLevels.has(Number(sub.id))
).length || 0)
: 0;
// Checkbox hanya aktif jika SEMUA item dalam kategori tersebut dichecklist // Checkbox hanya aktif jika SEMUA item dalam kategori tersebut dichecklist
const hasCheckedPolda = totalPolda > 0 && checkedPoldaCount === totalPolda; const hasCheckedPolda =
const hasCheckedPolres = totalPolres > 0 && checkedPolresCount === totalPolres; totalPolda > 0 && checkedPoldaCount === totalPolda;
const hasCheckedSatker = totalSatker > 0 && checkedSatkerCount === totalSatker; const hasCheckedPolres =
totalPolres > 0 && checkedPolresCount === totalPolres;
const hasCheckedSatker =
totalSatker > 0 && checkedSatkerCount === totalSatker;
// Update unitSelection berdasarkan checkbox yang aktif di modal // Update unitSelection berdasarkan checkbox yang aktif di modal
setUnitSelection(prev => ({ setUnitSelection((prev) => ({
...prev, ...prev,
polda: hasCheckedPolda, polda: hasCheckedPolda,
polres: hasCheckedPolres, polres: hasCheckedPolres,
satker: hasCheckedSatker, satker: hasCheckedSatker,
// allUnit hanya true jika semua kategori terpenuhi // allUnit hanya true jika semua kategori terpenuhi
allUnit: hasCheckedPolda && hasCheckedPolres && hasCheckedSatker allUnit: hasCheckedPolda && hasCheckedPolres && hasCheckedSatker,
})); }));
}, 0); }, 0);
}; };
@ -453,29 +475,41 @@ export default function FormTask() {
// Khusus untuk unchecklist POLRES: hanya unchecklist polres, pertahankan polda // Khusus untuk unchecklist POLRES: hanya unchecklist polres, pertahankan polda
if (penerimaTugasChangeType === "polres_unchecked") { if (penerimaTugasChangeType === "polres_unchecked") {
const newCheckedLevels = new Set<number>(checkedLevels); const newCheckedLevels = new Set<number>(checkedLevels);
// Hapus semua polres dari modal, tapi pertahankan polda // Hapus semua polres dari modal, tapi pertahankan polda
listDest.forEach((item: any) => { listDest.forEach((item: any) => {
if (item.subDestination && item.levelNumber === 2 && item.name !== "SATKER POLRI") { if (
item.subDestination &&
item.levelNumber === 2 &&
item.name !== "SATKER POLRI"
) {
item.subDestination.forEach((polres: any) => { item.subDestination.forEach((polres: any) => {
newCheckedLevels.delete(Number(polres.id)); newCheckedLevels.delete(Number(polres.id));
}); });
} }
}); });
setCheckedLevels(newCheckedLevels); setCheckedLevels(newCheckedLevels);
} }
// Untuk perubahan lainnya, jalankan logika normal // Untuk perubahan lainnya, jalankan logika normal
else if (unitSelection.polda || unitSelection.polres || unitSelection.satker) { else if (
unitSelection.polda ||
unitSelection.polres ||
unitSelection.satker
) {
// Mulai dengan checkbox yang sudah ada untuk mempertahankan pilihan manual user // Mulai dengan checkbox yang sudah ada untuk mempertahankan pilihan manual user
const newCheckedLevels = new Set<number>(checkedLevels); const newCheckedLevels = new Set<number>(checkedLevels);
listDest.forEach((item: any) => { listDest.forEach((item: any) => {
// Jika polda dichecklist, checklist semua polda (levelNumber 2, bukan SATKER POLRI) // Jika polda dichecklist, checklist semua polda (levelNumber 2, bukan SATKER POLRI)
if (unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { if (
unitSelection.polda &&
item.levelNumber === 2 &&
item.name !== "SATKER POLRI"
) {
newCheckedLevels.add(Number(item.id)); newCheckedLevels.add(Number(item.id));
} }
// Jika satker dichecklist, checklist SATKER POLRI dan sub-itemnya // Jika satker dichecklist, checklist SATKER POLRI dan sub-itemnya
if (unitSelection.satker && item.name === "SATKER POLRI") { if (unitSelection.satker && item.name === "SATKER POLRI") {
newCheckedLevels.add(Number(item.id)); newCheckedLevels.add(Number(item.id));
@ -485,17 +519,25 @@ export default function FormTask() {
}); });
} }
} }
// Jika polres dichecklist // Jika polres dichecklist
if (unitSelection.polres && item.subDestination) { if (unitSelection.polres && item.subDestination) {
// Jika checkbox POLDA di Penerima Tugas juga aktif, checklist semua polres // Jika checkbox POLDA di Penerima Tugas juga aktif, checklist semua polres
if (unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { if (
unitSelection.polda &&
item.levelNumber === 2 &&
item.name !== "SATKER POLRI"
) {
item.subDestination.forEach((polres: any) => { item.subDestination.forEach((polres: any) => {
newCheckedLevels.add(Number(polres.id)); newCheckedLevels.add(Number(polres.id));
}); });
} }
// Jika checkbox POLDA di Penerima Tugas tidak aktif, tapi ada POLDA yang dichecklist di modal // Jika checkbox POLDA di Penerima Tugas tidak aktif, tapi ada POLDA yang dichecklist di modal
else if (!unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { else if (
!unitSelection.polda &&
item.levelNumber === 2 &&
item.name !== "SATKER POLRI"
) {
// Cek apakah POLDA ini sudah dichecklist di modal // Cek apakah POLDA ini sudah dichecklist di modal
if (checkedLevels.has(Number(item.id))) { if (checkedLevels.has(Number(item.id))) {
// Jika ya, checklist semua polres dari POLDA ini // Jika ya, checklist semua polres dari POLDA ini
@ -506,13 +548,13 @@ export default function FormTask() {
} }
} }
}); });
setCheckedLevels(newCheckedLevels); setCheckedLevels(newCheckedLevels);
} else { } else {
// Jika tidak ada unitSelection yang aktif, unchecklist semua item di modal // Jika tidak ada unitSelection yang aktif, unchecklist semua item di modal
setCheckedLevels(new Set<number>()); setCheckedLevels(new Set<number>());
} }
// Reset flag setelah sinkronisasi selesai // Reset flag setelah sinkronisasi selesai
setTimeout(() => { setTimeout(() => {
setIsUpdatingFromPenerimaTugas(false); setIsUpdatingFromPenerimaTugas(false);
@ -756,7 +798,7 @@ export default function FormTask() {
</Select> </Select>
</div> </div>
<div className="flex flex-wrap gap-3 mt-5 lg:pt-7 lg:ml-3 "> <div className="flex flex-wrap gap-3 mt-5 lg:pt-7 lg:ml-3 ">
{Object.keys(unitSelection).map((key) => { {/* {Object.keys(unitSelection).map((key) => {
return ( return (
<div className="flex items-center gap-2" key={key}> <div className="flex items-center gap-2" key={key}>
<Checkbox <Checkbox
@ -776,7 +818,37 @@ export default function FormTask() {
</Label> </Label>
</div> </div>
); );
})} })} */}
{Object.keys(unitSelection)
.filter((key) => {
if (levelNumber === 2) {
return key === "polda" || key === "polres";
}
return true;
})
.map((key) => {
return (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={
unitSelection[key as keyof typeof unitSelection]
}
onCheckedChange={(value) =>
handleUnitChange(
key as keyof typeof unitSelection,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key === "allUnit"
? "Semua Unit"
: key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
);
})}
</div> </div>
<div className="mt-6 lg:pt-6 lg:pl-3"> <div className="mt-6 lg:pt-6 lg:pl-3">
<Dialog> <Dialog>
@ -790,77 +862,83 @@ export default function FormTask() {
<DialogTitle>Daftar Wilayah Polda dan Polres</DialogTitle> <DialogTitle>Daftar Wilayah Polda dan Polres</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto"> <div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listDest.map((polda: any) => ( {listDest.map((polda: any) => (
<div key={polda.id} className="border p-2"> <div key={polda.id} className="border p-2">
<Label className="flex items-center"> <Label className="flex items-center">
<Checkbox <Checkbox
checked={checkedLevels.has(Number(polda.id))} checked={checkedLevels.has(Number(polda.id))}
onCheckedChange={() => handleCheckboxChange(Number(polda.id))} onCheckedChange={() =>
className="mr-3" handleCheckboxChange(Number(polda.id))
/> }
{polda.name} className="mr-3"
<button />
type="button" {polda.name}
onClick={(e) => { <button
e.preventDefault(); type="button"
e.stopPropagation(); onClick={(e) => {
toggleExpand(polda.id); e.preventDefault();
}} e.stopPropagation();
className="ml-2 focus:outline-none" toggleExpand(polda.id);
> }}
{expandedPolda[polda.id] ? ( className="ml-2 focus:outline-none"
<ChevronUp size={16} /> >
) : ( {expandedPolda[polda.id] ? (
<ChevronDown size={16} /> <ChevronUp size={16} />
)} ) : (
</button> <ChevronDown size={16} />
</Label> )}
{expandedPolda[polda.id] && ( </button>
<div className="ml-6 mt-2"> </Label>
<Label className="block"> {expandedPolda[polda.id] && (
<Checkbox <div className="ml-6 mt-2">
checked={polda?.subDestination?.every( <Label className="block">
(polres: any) => <Checkbox
checkedLevels.has(Number(polres.id)) checked={polda?.subDestination?.every(
)} (polres: any) =>
onCheckedChange={(isChecked) => { checkedLevels.has(Number(polres.id))
const updatedLevels = new Set( )}
checkedLevels onCheckedChange={(isChecked) => {
); const updatedLevels = new Set(
polda?.subDestination?.forEach( checkedLevels
(polres: any) => { );
if (isChecked) { polda?.subDestination?.forEach(
updatedLevels.add(Number(polres.id)); (polres: any) => {
} else { if (isChecked) {
updatedLevels.delete(Number(polres.id)); updatedLevels.add(Number(polres.id));
} } else {
updatedLevels.delete(
Number(polres.id)
);
} }
); }
setCheckedLevels(updatedLevels); );
setCheckedLevels(updatedLevels);
// Update unitSelection berdasarkan perubahan
updateUnitSelectionFromModal(); // Update unitSelection berdasarkan perubahan
}} updateUnitSelectionFromModal();
}}
className="mr-2"
/>
Pilih Semua
</Label>
{polda?.subDestination?.map((polres: any) => (
<Label key={polres.id} className="block mt-1">
<Checkbox
checked={checkedLevels.has(
Number(polres.id)
)}
onCheckedChange={() =>
handleCheckboxChange(Number(polres.id))
}
className="mr-2" className="mr-2"
/> />
Pilih Semua {polres.name}
</Label> </Label>
{polda?.subDestination?.map((polres: any) => ( ))}
<Label key={polres.id} className="block mt-1"> </div>
<Checkbox )}
checked={checkedLevels.has(Number(polres.id))} </div>
onCheckedChange={() => ))}
handleCheckboxChange(Number(polres.id))
}
className="mr-2"
/>
{polres.name}
</Label>
))}
</div>
)}
</div>
))}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>