This commit is contained in:
Sabda Yagra 2026-02-13 19:44:21 +07:00
commit 11d47e4c29
15 changed files with 1046 additions and 292 deletions

View File

@ -427,14 +427,14 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
helpText="Group classification for organization" helpText="Group classification for organization"
/> />
<FormField {/* <FormField
label="Is Approval Active" label="Is Approval Active"
name={`isApprovalActive-${index}`} name={`isApprovalActive-${index}`}
type="checkbox" type="checkbox"
value={item.isApprovalActive} value={item.isApprovalActive}
onChange={(value) => onUpdate({ ...item, isApprovalActive: value })} onChange={(value) => onUpdate({ ...item, isApprovalActive: value })}
helpText="Users with this level can participate in approval process" helpText="Users with this level can participate in approval process"
/> /> */}
<FormField <FormField
label="Is Active" label="Is Active"
@ -1152,7 +1152,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField {/* <FormField
label="Is Approval Active" label="Is Approval Active"
name="isApprovalActive" name="isApprovalActive"
type="checkbox" type="checkbox"
@ -1161,7 +1161,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
handleFieldChange("isApprovalActive", value) handleFieldChange("isApprovalActive", value)
} }
helpText="Users with this level can participate in approval process" helpText="Users with this level can participate in approval process"
/> /> */}
<FormField <FormField
label="Is Active" label="Is Active"

View File

@ -31,6 +31,7 @@ type CategoryDetail = {
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
createdByFullname: string;
}; };
export default function CategoriesUpdateForm() { export default function CategoriesUpdateForm() {
@ -257,8 +258,9 @@ export default function CategoriesUpdateForm() {
<div> <div>
<Label>Created By</Label> <Label>Created By</Label>
<Input <Input
disabled
type="text" type="text"
value={formData.createdByName || formData.createdById} value={formData.createdByFullname}
readOnly readOnly
/> />
</div> </div>

View File

@ -94,6 +94,7 @@ type Detail = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
files: FileType[] | null; files: FileType[] | null;
publishedFor?: string | null;
categories: { categories: {
id: number; id: number;
title: string; title: string;
@ -161,6 +162,16 @@ export default function FormVideoDetail() {
fetchCategories(); fetchCategories();
}, []); }, []);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
useEffect(() => { useEffect(() => {
async function fetchDetail() { async function fetchDetail() {
if (!id) return; if (!id) return;
@ -175,9 +186,22 @@ export default function FormVideoDetail() {
uploadedById: details?.createdById, uploadedById: details?.createdById,
files: details?.files || [], files: details?.files || [],
thumbnailUrl: details?.thumbnailUrl || details?.thumbnail || "", thumbnailUrl: details?.thumbnailUrl || details?.thumbnail || "",
publishedFor: details?.publishedFor,
}; };
setDetail(mappedDetail); setDetail(mappedDetail);
setFiles(details?.files || []); setFiles(details?.files || []);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id); const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data); setApproval(approvals?.data?.data);
} catch (err) { } catch (err) {
@ -359,7 +383,60 @@ export default function FormVideoDetail() {
</div> </div>
</div> </div>
<div className="px-3 py-3 gap-2"> <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3 gap-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
{[5, 6].map((target) => ( {[5, 6].map((target) => (
<div key={target} className="flex items-center gap-2"> <div key={target} className="flex items-center gap-2">
@ -374,7 +451,7 @@ export default function FormVideoDetail() {
</Label> </Label>
</div> </div>
))} ))}
</div> </div> */}
<div className="px-3 py-3 border mx-3"> <div className="px-3 py-3 border mx-3">
<p>Information:</p> <p>Information:</p>

View File

@ -930,12 +930,11 @@ export default function FormVideo() {
} }
if (id == undefined) { if (id == undefined) {
// New Articles API request data structure
const articleData: CreateArticleData = { const articleData: CreateArticleData = {
aiArticleId: 0, // default 0 aiArticleId: 0,
categoryIds: selectedCategory.toString(), categoryIds: selectedCategory.toString(),
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend createdAt: formatDateForBackend(new Date()),
createdById: Number(userId), // isi dengan userId valid createdById: Number(userId),
description: htmlToString(finalDescription), description: htmlToString(finalDescription),
htmlDescription: finalDescription, htmlDescription: finalDescription,
isDraft: true, isDraft: true,
@ -948,9 +947,9 @@ export default function FormVideo() {
tags: finalTags, tags: finalTags,
title: finalTitle, title: finalTitle,
typeId: 2, typeId: 2,
publishedFor: data.publishedFor.join(","),
}; };
// Use new Articles API
const response = await createArticle(articleData); const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData); console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response); console.log("Article API Response:", response);
@ -1651,7 +1650,8 @@ export default function FormVideo() {
<p className="text-sm font-semibold">Content Rewrite</p> <p className="text-sm font-semibold">Content Rewrite</p>
<div className="my-2"> <div className="my-2">
<button type="button" <button
type="button"
onClick={handleRewriteClick} onClick={handleRewriteClick}
className="bg-blue-500 text-white py-2 px-4 rounded" className="bg-blue-500 text-white py-2 px-4 rounded"
> >

View File

@ -177,7 +177,13 @@ export default function FormVideoUpdate() {
setSelectedCategory(String(detailData.categories[0].id)); setSelectedCategory(String(detailData.categories[0].id));
setTags(detailData.tags?.split(",").map((t: string) => t.trim()) || []); setTags(detailData.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(detailData.publishedFor?.split(",") || []); // setPublishedFor(detailData.publishedFor?.split(",") || []);
const publishTargets = detailData?.publishedFor
? detailData.publishedFor.split(",")
: [];
setPublishedFor(publishTargets);
setValue("publishedFor", publishTargets);
} catch (err) { } catch (err) {
close(); close();
console.error("❌ Error loading detail:", err); console.error("❌ Error loading detail:", err);
@ -218,6 +224,7 @@ export default function FormVideoUpdate() {
title: data.title, title: data.title,
typeId: detail.typeId, typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"), slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
}; };
// const payload = { // const payload = {
@ -282,6 +289,17 @@ export default function FormVideoUpdate() {
if (!detail) return <p className="p-5 text-center">Memuat data...</p>; if (!detail) return <p className="p-5 text-center">Memuat data...</p>;
const getVideoSrc = (url?: string) => {
if (!url) return "";
// kalau sudah absolute
if (url.startsWith("http")) return url;
// kalau backend kirim relative path
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
return `${baseUrl}${url}`;
};
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg"> <div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
@ -363,21 +381,24 @@ export default function FormVideoUpdate() {
{detailFiles.map((file) => ( {detailFiles.map((file) => (
<div key={file.id} className="flex flex-row items-center gap-4"> <div key={file.id} className="flex flex-row items-center gap-4">
<video <video
className="object-contain w-[300px] h-[200px]" className="object-contain w-[300px] h-[200px] rounded border"
src={file.fileUrl} src={getVideoSrc(file.fileUrl)}
controls controls
preload="metadata"
title={file.fileName} title={file.fileName}
/> />
<p>{file.fileName}</p> <p>{file.fileName}</p>
<a <button
type="button"
className="text-destructive" className="text-destructive"
onClick={() => handleDeleteFile(file.id)} onClick={() => handleDeleteFile(file.id)}
> >
<TimesIcon /> <TimesIcon />
</a> </button>
</div> </div>
))} ))}
{files?.map((file) => ( {files?.map((file) => (
<div <div
key={file.name} key={file.name}
@ -466,6 +487,68 @@ export default function FormVideoUpdate() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<Controller <Controller
control={control}
name="publishedFor"
render={({ field }) => {
const isAllChecked =
field.value?.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: field.value?.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...(field.value || []), option.id]
: field.value?.filter(
(val) => val !== option.id,
) || [];
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) => handleChange(e.target.checked)}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control} control={control}
name="publishedFor" name="publishedFor"
render={({ field }) => ( render={({ field }) => (
@ -512,13 +595,6 @@ export default function FormVideoUpdate() {
onChange={(e) => handleChange(e.target.checked)} onChange={(e) => handleChange(e.target.checked)}
className="border" className="border"
/> />
{/* <Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
className="border"
/> */}
<Label htmlFor={option.id}>{option.label}</Label> <Label htmlFor={option.id}>{option.label}</Label>
</div> </div>
); );
@ -532,7 +608,7 @@ export default function FormVideoUpdate() {
</div> </div>
</div> </div>
)} )}
/> /> */}
</div> </div>
<div className="flex justify-end gap-3 mt-5"> <div className="flex justify-end gap-3 mt-5">

View File

@ -91,25 +91,63 @@ type FileType = {
}; };
type Detail = { type Detail = {
id: string; id: number;
title: string; title: string;
description: string; description: string;
htmlDescription: string;
slug: string; slug: string;
category: { categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: FileType[] | null;
publishedFor?: string | null;
categories: {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}[];
// Legacy fields for backward compatibility
category?: {
id: number; id: number;
name: string; name: string;
}; };
categoryName: string; creatorName?: string;
creatorName: string; thumbnailLink?: string;
thumbnailLink: string; statusName?: string;
tags: string; needApprovalFromLevel?: number;
statusName: string; uploadedById?: number;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
}; };
const ViewEditor = dynamic( const ViewEditor = dynamic(
() => { () => {
return import("@/components/editor/view-editor"); return import("@/components/editor/view-editor");
@ -219,6 +257,16 @@ export default function FormAudioDetail() {
} }
}, [userLevelId, roleId]); }, [userLevelId, roleId]);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id: any) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
const handleCheckboxChange = (id: number) => { const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) => setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
@ -266,6 +314,18 @@ export default function FormAudioDetail() {
}); });
setupPlacementCheck(details?.files?.length); setupPlacementCheck(details?.files?.length);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
if (details?.publishedForObject) { if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject.map( const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id, (obj: any) => obj.id,
@ -611,6 +671,58 @@ export default function FormAudioDetail() {
</div> </div>
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2"> <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@ -632,7 +744,7 @@ export default function FormAudioDetail() {
<Label htmlFor="6">JOURNALIS</Label> <Label htmlFor="6">JOURNALIS</Label>
</div> </div>
</div> </div>
</div> </div> */}
<SuggestionModal <SuggestionModal
id={Number(id)} id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion} numberOfSuggestion={detail?.numberOfSuggestion}

View File

@ -650,6 +650,7 @@ export default function FormAudio() {
tags: finalTags, tags: finalTags,
title: finalTitle, title: finalTitle,
typeId: 4, typeId: 4,
publishedFor: data.publishedFor.join(","),
}; };
// Use new Articles API // Use new Articles API

View File

@ -250,6 +250,14 @@ export default function FormAudioUpdate() {
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []); setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(details.publishedFor?.split(",") || []); setPublishedFor(details.publishedFor?.split(",") || []);
if (details?.publishedFor) {
const parsed = details.publishedFor
.split(",")
.map((id: string) => id.trim());
setValue("publishedFor", parsed); // 🔥 WAJIB
}
if (details?.files) { if (details?.files) {
setPrefFiles(details.files); setPrefFiles(details.files);
// setFiles(details.files); // setFiles(details.files);
@ -362,6 +370,7 @@ export default function FormAudioUpdate() {
title: data.title, title: data.title,
typeId: detail.typeId, typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"), slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
}; };
// const payload = { // const payload = {
// aiArticleId: detail?.aiArticleId ?? "", // aiArticleId: detail?.aiArticleId ?? "",
@ -560,50 +569,87 @@ export default function FormAudioUpdate() {
setFiles([...filtered]); setFiles([...filtered]);
}; };
const fileList = files.map((file: any) => ( const getAudioUrl = (file: any) => {
if (file instanceof File) {
return URL.createObjectURL(file);
}
return file.secondaryUrl || file.fileUrl || null;
};
const fileList = files.map((file: any) => {
const audioSrc = getAudioUrl(file);
return (
<div <div
key={file.id} // Gunakan ID file sebagai key key={file.id || file.name}
className="flex justify-between border px-3.5 py-3 my-6 rounded-md" className="flex flex-col gap-2 border p-3 my-6 rounded-md"
> >
<div className="flex gap-3 items-center"> <p className="text-sm font-medium truncate">{file.name}</p>
<svg
xmlns="http://www.w3.org/2000/svg" {audioSrc && (
width="48" <audio controls className="w-full">
height="48" <source src={audioSrc} type={file.type || "audio/mpeg"} />
viewBox="0 0 20 20" Browser tidak mendukung audio.
> </audio>
<path
fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/>
</svg>{" "}
<div>
<div className="text-sm text-card-foreground">
{file.fileName || file.name}
</div>
<div className="text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)} )}
{" kb"}
</div>
</div>
</div>
<Button <Button
type="button" type="button"
size="icon" size="icon"
color="destructive"
variant="outline" variant="outline"
className="border-none rounded-full" className="self-end"
onClick={() => handleRemoveFile(file)} // Kirim ID spesifik onClick={() => handleRemoveFile(file)}
> >
<Icon icon="tabler:x" className="h-5 w-5" /> <Icon icon="tabler:x" className="h-5 w-5" />
</Button> </Button>
</div> </div>
)); );
});
// const fileList = files.map((file: any) => (
// <div
// key={file.id} // Gunakan ID file sebagai key
// className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
// >
// <div className="flex gap-3 items-center">
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="48"
// height="48"
// viewBox="0 0 20 20"
// >
// <path
// fill="currentColor"
// d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
// />
// </svg>{" "}
// <div>
// <div className="text-sm text-card-foreground">
// {file.fileName || file.name}
// </div>
// <div className="text-xs font-light text-muted-foreground">
// {Math.round(file.size / 100) / 10 > 1000 ? (
// <>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
// ) : (
// <>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
// )}
// {" kb"}
// </div>
// </div>
// </div>
// <Button
// type="button"
// size="icon"
// color="destructive"
// variant="outline"
// className="border-none rounded-full"
// onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
// >
// <Icon icon="tabler:x" className="h-5 w-5" />
// </Button>
// </div>
// ));
const handleCheckboxChangeImage = (fileId: number, value: string) => { const handleCheckboxChangeImage = (fileId: number, value: string) => {
setSelectedOptions((prev: any) => { setSelectedOptions((prev: any) => {
@ -792,9 +838,45 @@ export default function FormAudioUpdate() {
</Fragment> </Fragment>
) : null} ) : null}
{prevFiles?.length > 0 && {prevFiles?.length > 0 &&
prevFiles.map((file: any) => {
const audioSrc = file.secondaryUrl || file.fileUrl;
return (
<div
key={file.id}
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
>
<p className="text-sm font-medium truncate">
{file.fileName}
</p>
{audioSrc ? (
<audio controls className="w-full">
<source src={audioSrc} type="audio/mpeg" />
Browser tidak mendukung audio.
</audio>
) : (
<p className="text-xs text-red-500">
Audio source tidak tersedia
</p>
)}
<Button
size="icon"
variant="outline"
className="self-end"
onClick={() => handleDeleteFile(file.id)}
>
<Icon icon="tabler:x" className="h-5 w-5" />
</Button>
</div>
);
})}
{/* {prevFiles?.length > 0 &&
prevFiles.map((file: any) => ( prevFiles.map((file: any) => (
<div <div
key={file.id} // Gunakan ID file sebagai key key={file.id}
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"
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
@ -842,7 +924,7 @@ export default function FormAudioUpdate() {
<Icon icon="tabler:x" className="h-5 w-5" /> <Icon icon="tabler:x" className="h-5 w-5" />
</Button> </Button>
</div> </div>
))} ))} */}
{/* {files.length > 0 && ( {/* {files.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<Label className="text-lg font-semibold"> <Label className="text-lg font-semibold">
@ -1012,6 +1094,70 @@ export default function FormAudioUpdate() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<Controller <Controller
control={control}
name="publishedFor"
render={({ field }) => {
const isAllChecked =
field.value?.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: field.value?.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...(field.value || []), option.id]
: field.value?.filter(
(val) => val !== option.id,
) || [];
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) =>
handleChange(e.target.checked)
}
/>
<Label htmlFor={option.id}>{option.name}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control} control={control}
name="publishedFor" name="publishedFor"
render={({ field }) => ( render={({ field }) => (
@ -1079,7 +1225,7 @@ export default function FormAudioUpdate() {
</div> </div>
</div> </div>
)} )}
/> /> */}
</div> </div>
</div> </div>
{/* <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm"> {/* <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">

View File

@ -94,23 +94,62 @@ type FileType = {
}; };
type Detail = { type Detail = {
id: string; id: number;
title: string; title: string;
description: string; description: string;
htmlDescription: string;
slug: string; slug: string;
category: { categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: FileType[] | null;
publishedFor?: string | null;
categories: {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}[];
// Legacy fields for backward compatibility
category?: {
id: number; id: number;
name: string; name: string;
}; };
categoryName: string; creatorName?: string;
creatorName: string; thumbnailLink?: string;
thumbnailLink: string; statusName?: string;
tags: string; needApprovalFromLevel?: number;
statusName: string; uploadedById?: number;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
}; };
const ViewEditor = dynamic( const ViewEditor = dynamic(
@ -208,6 +247,16 @@ export default function FormTeksDetail() {
initState(); initState();
}, []); }, []);
useEffect(() => {
if (!detail?.publishedFor) return;
const publisherIds = detail.publishedFor
.split(",")
.map((id: any) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}, [detail]);
const getCategories = async () => { const getCategories = async () => {
try { try {
const categoryRes = await listArticleCategories(1, 100); const categoryRes = await listArticleCategories(1, 100);
@ -244,6 +293,18 @@ export default function FormTeksDetail() {
setFiles(details?.files || []); setFiles(details?.files || []);
setDetail(details); setDetail(details);
// 🔥 Parse publish target seperti content image
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
// ✅ Aman untuk fileType // ✅ Aman untuk fileType
setMain({ setMain({
type: details?.fileType?.name || "Unknown", type: details?.fileType?.name || "Unknown",
@ -563,6 +624,58 @@ export default function FormTeksDetail() {
</div> </div>
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5"
value="5"
checked={selectedPublishers.includes(5)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="4"
checked={selectedPublishers.includes(5)}
disabled
/>
<Label htmlFor="4">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(6)}
disabled
/>
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div> */}
</div>
{/* <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2"> <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@ -584,7 +697,7 @@ export default function FormTeksDetail() {
<Label htmlFor="6">JOURNALIS</Label> <Label htmlFor="6">JOURNALIS</Label>
</div> </div>
</div> </div>
</div> </div> */}
<SuggestionModal <SuggestionModal
id={Number(id)} id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion} numberOfSuggestion={detail?.numberOfSuggestion}

View File

@ -653,6 +653,7 @@ export default function FormTeks() {
tags: finalTags, tags: finalTags,
title: finalTitle, title: finalTitle,
typeId: 3, typeId: 3,
publishedFor: data.publishedFor.join(","),
}; };
const response = await createArticle(articleData); const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData); console.log("Article Data Submitted:", articleData);

View File

@ -16,6 +16,7 @@ import * as z from "zod";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { v4 as uuidv4 } from "uuid";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -88,6 +89,12 @@ type Category = {
title: string; title: string;
}; };
interface DetailFile {
id: number;
fileUrl: string;
fileName: string;
}
type Detail = { type Detail = {
id: string; id: string;
title: string; title: string;
@ -110,6 +117,7 @@ type Detail = {
interface FileWithPreview extends File { interface FileWithPreview extends File {
preview: string; preview: string;
id: string;
} }
type Option = { type Option = {
@ -153,6 +161,7 @@ export default function FormTeksUpdate() {
[fileId: number]: string[]; [fileId: number]: string[];
}>({}); }>({});
const [selectedTarget, setSelectedTarget] = useState(""); const [selectedTarget, setSelectedTarget] = useState("");
const [detailFiles, setDetailFiles] = useState<DetailFile[]>([]);
const [unitSelection, setUnitSelection] = useState({ const [unitSelection, setUnitSelection] = useState({
allUnit: false, allUnit: false,
mabes: false, mabes: false,
@ -161,18 +170,26 @@ export default function FormTeksUpdate() {
}); });
const [publishedFor, setPublishedFor] = useState<string[]>([]); const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [existingFiles, setExistingFiles] = useState<DetailFile[]>([]);
let fileTypeId = "3"; let fileTypeId = "3";
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file))); setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
id: uuidv4(),
preview: URL.createObjectURL(file),
}),
),
);
}, },
accept: { accept: {
"application/pdf": [], "application/pdf": [],
"application/msword": [], // .doc "application/msword": [],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[], // .docx [],
}, },
}); });
@ -219,9 +236,15 @@ export default function FormTeksUpdate() {
setValue("creatorName", details.createdByName ?? ""); setValue("creatorName", details.createdByName ?? "");
setTags(details.tags?.split(",").map((t: string) => t.trim()) || []); setTags(details.tags?.split(",").map((t: string) => t.trim()) || []);
setPublishedFor(details.publishedFor?.split(",") || []); setPublishedFor(details.publishedFor?.split(",") || []);
if (details?.publishedFor) {
const publishArr = details.publishedFor.split(",");
setPublishedFor(publishArr); // state lokal
setValue("publishedFor", publishArr); // ← WAJIB untuk react-hook-form
}
if (details?.files) { if (details?.files) {
setFiles(details.files); setExistingFiles(details.files);
const initialOptions: { [key: number]: string[] } = {}; const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => { details.files.forEach((file: any) => {
if (file.placements) { if (file.placements) {
@ -278,7 +301,6 @@ export default function FormTeksUpdate() {
return allSelected ? ["all", ...options] : options; return allSelected ? ["all", ...options] : options;
}; };
const handleCheckboxChange = (id: string) => { const handleCheckboxChange = (id: string) => {
if (id === "all") { if (id === "all") {
// Select all options except "all" // Select all options except "all"
@ -326,6 +348,7 @@ export default function FormTeksUpdate() {
title: data.title, title: data.title,
typeId: detail.typeId, typeId: detail.typeId,
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"), slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
publishedFor: data.publishedFor.join(","),
}; };
// const payload = { // const payload = {
// aiArticleId: detail?.aiArticleId ?? "", // aiArticleId: detail?.aiArticleId ?? "",
@ -678,153 +701,143 @@ export default function FormTeksUpdate() {
</p> </p>
)} )}
</div> </div>
<div className="py-3 space-y-2">
<Fragment>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>Select File</Label> <Label>Select File</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment> <Fragment>
<div {...getRootProps({ className: "dropzone" })}> <div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} /> <input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col"> <div className="w-full text-center border-dashed border border-black rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" /> <CloudUpload className="w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80"> <h4 className="text-2xl font-medium mt-3">
{/* Drop files here or click to upload. */}
Drag File Drag File
</h4> </h4>
<div className=" text-xs text-muted-foreground">
Upload File Text Max
</div> </div>
</div> </div>
</div>
{files.length ? ( {/* 👇 TARUH DI SINI */}
<Fragment> {files.length > 0 && (
<div>{fileList}</div> <div className="mt-4 space-y-2">
<div className=" flex justify-between gap-2"> <Label className="text-lg font-semibold">
{/* <div className="flex flex-row items-center gap-3 py-3"> File Baru
<Label>Watermark</Label> </Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" /> {files.map((file) => (
</div> <div
</div> */} key={file.name}
<Button className="flex justify-between items-center border p-3 rounded-md"
color="destructive"
onClick={handleRemoveAllFiles}
> >
Remove All <div>
<p className="font-medium">{file.name}</p>
<p className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(1)} KB
</p>
</div>
<Button
type="button"
size="icon"
variant="outline"
onClick={() =>
setFiles((prev) =>
prev.filter(
(item) => item.name !== file.name,
),
)
}
>
</Button> </Button>
</div> </div>
))}
</div>
)}
</Fragment> </Fragment>
) : null} </div>
{files.length > 0 && (
<></> {/* {files.length > 0 && (
// <div className="mt-4 space-y-2"> <div className="mt-4">
// <Label className="text-lg font-semibold"> <Label className="text-md font-semibold">
// {" "} File Media
// File Media </Label>
// </Label>
// <div className="grid gap-4"> <div className="grid gap-4">
// {files.map((file: any) => ( {files.map((file: any, index: number) => (
// <div <div
// key={file.id} key={file.id}
// className="flex items-center border p-2 rounded-md" className="flex items-center border p-2 rounded-md"
// > >
// <img {file.preview ? (
// src={file.thumbnailFileUrl} <img
// alt={file.fileName} src={file.preview}
// className="w-16 h-16 object-cover rounded-md mr-4" alt={file.name}
// /> className="w-16 h-16 object-cover rounded-md mr-4"
// <div className="flex flex-wrap gap-3 items-center "> />
// <div className="flex-grow"> ) : (
// <p className="font-medium">{file.fileName}</p> <Icon
// <a icon="tabler:file-description"
// href={file.url} className="w-16 h-16"
// target="_blank" />
// rel="noopener noreferrer" )}
// className="text-blue-500 text-sm"
// > <div className="flex-grow">
// View File <p className="font-medium">
// </a> {file.fileName || file.name}
// </div> </p>
// <div> </div>
// <Label className="flex items-center space-x-2">
// <input <Button
// type="checkbox" type="button"
// checked={selectedOptions[ variant="outline"
// file.id size="icon"
// ]?.includes("all")} onClick={() =>
// onChange={() => setFiles((prev) =>
// handleCheckboxChangeImage( prev.filter((f) => f.id !== file.id),
// file.id, )
// "all" }
// ) >
// }
// className="form-checkbox" </Button>
// /> </div>
// <span>All</span> ))}
// </Label> </div>
// </div> </div>
// <div> )} */}
// <Label className="flex items-center space-x-2">
// <input {/* Existing Files */}
// type="checkbox" {existingFiles.length > 0 && (
// checked={selectedOptions[ <div className="mt-4 space-y-2">
// file.id <Label className="text-lg font-semibold">
// ]?.includes("nasional")} File Sebelumnya
// onChange={() => </Label>
// handleCheckboxChangeImage(
// file.id, {existingFiles.map((file) => (
// "nasional" <div
// ) key={file.id}
// } className="flex justify-between items-center border p-3 rounded-md"
// className="form-checkbox" >
// /> <div className="flex items-center gap-3">
// <span>Nasional</span> <Icon
// </Label> icon="tabler:file-description"
// </div> className="w-10 h-10"
// <div> />
// <Label className="flex items-center space-x-2"> <div>
// <input <p className="font-medium">{file.fileName}</p>
// type="checkbox" <a
// checked={selectedOptions[ href={file.fileUrl}
// file.id target="_blank"
// ]?.includes("wilayah")} rel="noopener noreferrer"
// onChange={() => className="text-blue-500 text-sm"
// handleCheckboxChangeImage( >
// file.id, Lihat File
// "wilayah" </a>
// ) </div>
// } </div>
// className="form-checkbox" </div>
// /> ))}
// <span>Wilayah</span> </div>
// </Label>
// </div>
// <div>
// <Label className="flex items-center space-x-2">
// <input
// type="checkbox"
// checked={selectedOptions[
// file.id
// ]?.includes("internasional")}
// onChange={() =>
// handleCheckboxChangeImage(
// file.id,
// "internasional"
// )
// }
// className="form-checkbox"
// />
// <span>Internasional</span>
// </Label>
// </div>
// </div>
// </div>
// ))}
// </div>
// </div>
)} )}
</Fragment> </Fragment>
</div> </div>
@ -852,11 +865,18 @@ export default function FormTeksUpdate() {
{/* <div className="mt-3 px-3"> {/* <div className="mt-3 px-3">
<Label>Pratinjau Gambar Utama</Label> <Label>Pratinjau Gambar Utama</Label>
<Card className="mt-2"> <Card className="mt-2">
{files.preview ? (
<img <img
src={detail.thumbnailLink} src={files.preview}
alt="Thumbnail Gambar Utama" alt={files.name}
className="w-full h-auto rounded" className="w-16 h-16 object-cover rounded-md mr-4"
/> />
) : (
<Icon
icon="tabler:file-description"
className="w-16 h-16"
/>
)}
</Card> </Card>
</div> */} </div> */}
<div className="px-3 py-3"> <div className="px-3 py-3">
@ -898,6 +918,76 @@ export default function FormTeksUpdate() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
<Controller <Controller
control={control}
name="publishedFor"
render={({ field }) => {
const currentValue = field.value || [];
const isAllChecked =
currentValue.length ===
options.filter((opt) => opt.id !== "all").length;
return (
<div className="flex flex-col gap-3">
{options.map((option) => {
const isChecked =
option.id === "all"
? isAllChecked
: currentValue.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id)
: [];
} else {
updated = checked
? [...currentValue, option.id]
: currentValue.filter(
(val) => val !== option.id,
);
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex items-center gap-2"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) =>
handleChange(e.target.checked)
}
/>
<Label htmlFor={option.id}>
{option.label}
</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
);
}}
/>
{/* <Controller
control={control} control={control}
name="publishedFor" name="publishedFor"
render={({ field }) => ( render={({ field }) => (
@ -967,7 +1057,7 @@ export default function FormTeksUpdate() {
</div> </div>
</div> </div>
)} )}
/> /> */}
</div> </div>
</div> </div>
<div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm"> <div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">

View File

@ -127,6 +127,7 @@ type Detail = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
files: FileType[] | null; files: FileType[] | null;
published_for?: string;
categories: { categories: {
id: number; id: number;
title: string; title: string;
@ -322,6 +323,20 @@ export default function FormImageDetail() {
try { try {
const response = await getArticleDetail(Number(id)); const response = await getArticleDetail(Number(id));
const details = response?.data?.data; const details = response?.data?.data;
console.log("DETAIL RESPONSE:", details);
// ===== PARSE published_for =====
const rawPublished =
details?.published_for || details?.publishedFor || "";
if (rawPublished) {
const publisherIds = rawPublished
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
const mappedDetail: Detail = { const mappedDetail: Detail = {
...details, ...details,
category: category:
@ -351,6 +366,14 @@ export default function FormImageDetail() {
setFiles(mappedFiles); setFiles(mappedFiles);
setDetail(mappedDetail); setDetail(mappedDetail);
if (details?.published_for) {
const publisherIds = details.published_for
.split(",")
.map((id: string) => Number(id.trim()));
setSelectedPublishers(publisherIds);
}
if (mappedFiles && mappedFiles.length > 0) { if (mappedFiles && mappedFiles.length > 0) {
setMain({ setMain({
type: "image", type: "image",
@ -369,13 +392,13 @@ export default function FormImageDetail() {
setDetailThumb(fileUrls); setDetailThumb(fileUrls);
if (details?.publishedForObject?.length > 0) { // if (details?.publishedForObject?.length > 0) {
const publisherIds = details.publishedForObject // const publisherIds = details.publishedForObject
.map((obj: any) => Number(obj.id)) // .map((obj: any) => Number(obj.id))
.filter((id: number) => id === 5 || id === 6); // .filter((id: number) => id === 4 || id === 5);
setSelectedPublishers(publisherIds); // setSelectedPublishers(publisherIds);
} // }
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id); const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data); setApproval(approvals?.data?.data);
@ -773,36 +796,54 @@ export default function FormImageDetail() {
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-2 space-y-2"> <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label> <Label>Publish Target</Label>
{/* UMUM = 4 */}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* <Checkbox <input
type="checkbox"
id="4"
value="4"
checked={selectedPublishers.includes(4)}
readOnly
className="h-4 w-4 border border-gray-300 rounded"
/>
<Label htmlFor="4">UMUM</Label>
</div>
{/* JOURNALIS = 5 */}
<div className="flex gap-2 items-center">
<input
type="checkbox"
id="5" id="5"
value="5"
checked={selectedPublishers.includes(5)} checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)} readOnly
className="border" className="h-4 w-4 border border-gray-300 rounded"
/> */} />
<Label htmlFor="5">JOURNALIS</Label>
</div>
</div>
{/* <div className="flex flex-col gap-2 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox <Checkbox
id="5" id="4"
checked={selectedPublishers.includes(5)} checked={selectedPublishers.includes(5)}
disabled disabled
/> />
<Label htmlFor="5">UMUM</Label> <Label htmlFor="4">UMUM</Label>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* <Checkbox
id="6"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
className="border"
/> */}
<Checkbox <Checkbox
id="6" id="5"
checked={selectedPublishers.includes(6)} checked={selectedPublishers.includes(6)}
disabled disabled
/> />
<Label htmlFor="6">JOURNALIS</Label> <Label htmlFor="5">JOURNALIS</Label>
</div>
</div> </div>
</div> */}
</div> </div>
<SuggestionModal <SuggestionModal

View File

@ -557,7 +557,7 @@ export default function FormImage() {
} }
}, [articleBody, setValue]); }, [articleBody, setValue]);
const userId = Cookies.get("userId"); // atau dari auth context / localStorage const userId = Cookies.get("userId");
const save = async (data: ImageSchema) => { const save = async (data: ImageSchema) => {
loading(); loading();
@ -599,10 +599,10 @@ export default function FormImage() {
// ✅ Sesuaikan dengan struktur Swagger // ✅ Sesuaikan dengan struktur Swagger
const articleData: CreateArticleData = { const articleData: CreateArticleData = {
aiArticleId: 0, // default 0 aiArticleId: 0,
categoryIds: selectedCategory.toString(), categoryIds: selectedCategory.toString(),
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend createdAt: formatDateForBackend(new Date()),
createdById: Number(userId), // isi dengan userId valid createdById: Number(userId),
description: htmlToString(finalDescription), description: htmlToString(finalDescription),
htmlDescription: finalDescription, htmlDescription: finalDescription,
isDraft: true, isDraft: true,
@ -614,9 +614,31 @@ export default function FormImage() {
.replace(/[^a-z0-9-]/g, ""), .replace(/[^a-z0-9-]/g, ""),
tags: finalTags, tags: finalTags,
title: finalTitle, title: finalTitle,
typeId: 1, // Image content type typeId: 1,
// 🔥 TAMBAHKAN INI
publishedFor: data.publishedFor.join(","),
}; };
// const articleData: CreateArticleData = {
// aiArticleId: 0, // default 0
// categoryIds: selectedCategory.toString(),
// createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
// createdById: Number(userId), // isi dengan userId valid
// description: htmlToString(finalDescription),
// htmlDescription: finalDescription,
// isDraft: true,
// isPublish: false,
// oldId: 0,
// slug: finalTitle
// .toLowerCase()
// .replace(/\s+/g, "-")
// .replace(/[^a-z0-9-]/g, ""),
// tags: finalTags,
// title: finalTitle,
// typeId: 1, // Image content type
// };
let id = Cookies.get("idCreate"); let id = Cookies.get("idCreate");
if (id == undefined) { if (id == undefined) {

View File

@ -59,7 +59,7 @@ import { useTranslations } from "next-intl";
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"), () => import("@/components/editor/custom-editor"),
{ ssr: false } { ssr: false },
); );
const imageSchema = z.object({ const imageSchema = z.object({
@ -99,7 +99,7 @@ export default function FormImageUpdate() {
setFiles((prev) => [ setFiles((prev) => [
...prev, ...prev,
...acceptedFiles.map((f) => ...acceptedFiles.map((f) =>
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }) Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }),
), ),
]), ]),
accept: { "image/*": [] }, accept: { "image/*": [] },
@ -171,12 +171,13 @@ export default function FormImageUpdate() {
const allOptions = options const allOptions = options
.filter((opt) => opt.id !== "all") .filter((opt) => opt.id !== "all")
.map((opt) => opt.id); .map((opt) => opt.id);
setPublishedFor( setPublishedFor(
publishedFor.length === allOptions.length ? [] : allOptions publishedFor.length === allOptions.length ? [] : allOptions,
); );
} else { } else {
setPublishedFor((prev) => setPublishedFor((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id] prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
); );
} }
}; };
@ -233,6 +234,23 @@ export default function FormImageUpdate() {
// router.push("/admin/content/image"); // router.push("/admin/content/image");
// }); // });
// }; // };
const formatDateTime = (date: Date) => {
const pad = (n: number) => n.toString().padStart(2, "0");
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
};
// 🔹 ganti fungsi save di FormImageUpdate.tsx // 🔹 ganti fungsi save di FormImageUpdate.tsx
const save = async (data: ImageSchema) => { const save = async (data: ImageSchema) => {
@ -248,7 +266,10 @@ export default function FormImageUpdate() {
const payload = { const payload = {
aiArticleId: detail?.aiArticleId ?? null, aiArticleId: detail?.aiArticleId ?? null,
categoryIds: selectedTarget ? String(selectedTarget) : "", categoryIds: selectedTarget ? String(selectedTarget) : "",
createdAt: detail?.createdAt ?? new Date().toISOString(), // createdAt: detail?.createdAt ?? new Date().toISOString(),
createdAt: detail?.createdAt
? detail.createdAt.replace("T", " ").split("+")[0]
: formatDateTime(new Date()),
createdById: detail?.createdById ?? null, createdById: detail?.createdById ?? null,
description: htmlToString(descFinal), description: htmlToString(descFinal),
htmlDescription: descFinal, htmlDescription: descFinal,
@ -261,9 +282,9 @@ export default function FormImageUpdate() {
.replace(/[^a-z0-9]+/g, "-") .replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, ""), .replace(/(^-|-$)+/g, ""),
statusId: detail?.statusId ?? 1, statusId: detail?.statusId ?? 1,
tags: tags, tags: tags.join(", "),
title: data.title, title: data.title,
typeId: 1, // 1 = image (sesuai struktur kamu) typeId: 1,
}; };
console.log("📤 Payload Update Article:", payload); console.log("📤 Payload Update Article:", payload);
@ -379,14 +400,14 @@ export default function FormImageUpdate() {
!categories?.find( !categories?.find(
(cat) => (cat) =>
String(cat.id) === String(cat.id) ===
String(detail.categoryId || detail?.category?.id) String(detail.categoryId || detail?.category?.id),
) && ( ) && (
<SelectItem <SelectItem
key={String( key={String(
detail.categoryId || detail?.category?.id detail.categoryId || detail?.category?.id,
)} )}
value={String( value={String(
detail.categoryId || detail?.category?.id detail.categoryId || detail?.category?.id,
)} )}
> >
{detail.categoryName || detail?.category?.name} {detail.categoryName || detail?.category?.name}
@ -447,7 +468,7 @@ export default function FormImageUpdate() {
className="flex justify-between border p-3 rounded-md" className="flex justify-between border p-3 rounded-md"
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Image {/* <Image
src={ src={
file.thumbnailFileUrl || file.thumbnailFileUrl ||
file.preview || file.preview ||
@ -457,11 +478,25 @@ export default function FormImageUpdate() {
width={64} width={64}
height={64} height={64}
className="rounded border" className="rounded border"
/> */}
<Image
src={
file.preview || // file baru (dropzone)
file.thumbnailUrl || // dari backend jika ada
file.fileUrl || // fallback utama dari backend
"/placeholder.png"
}
alt={file.fileName || file.name || "file"}
width={64}
height={64}
className="rounded border object-cover"
unoptimized
/> />
<div> <div>
<p className="font-medium">{file.fileName}</p> <p className="font-medium">{file.fileName}</p>
<a <a
href={file.url} href={file.fileUrl || file.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-500 text-sm" className="text-blue-500 text-sm"
@ -554,6 +589,36 @@ export default function FormImageUpdate() {
</div> </div>
<div> <div>
<Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2">
{options.map((opt) => {
const isAllSelected =
publishedFor.length ===
options.filter((o) => o.id !== "all").length;
const isChecked =
opt.id === "all"
? isAllSelected
: publishedFor.includes(opt.id);
return (
<div key={opt.id} className="flex items-center gap-2">
<input
type="checkbox"
id={opt.id}
value={opt.id}
checked={isChecked}
onChange={() => handleCheckboxChange(opt.id)}
className="w-4 h-4 accent-black"
/>
<Label htmlFor={opt.id}>{opt.name}</Label>
</div>
);
})}
</div>
</div>
{/* <div>
<Label>Publish Target</Label> <Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2"> <div className="flex flex-col gap-2 mt-2">
{options.map((opt) => ( {options.map((opt) => (
@ -572,12 +637,19 @@ export default function FormImageUpdate() {
</div> </div>
))} ))}
</div> </div>
</div> </div> */}
</Card> </Card>
<div className="flex justify-end gap-3 mt-4"> <div className="flex justify-end gap-3 mt-4">
<Button type="submit">Simpan</Button>
<Button <Button
variant="outline"
type="submit"
className="hover:bg-gray-300"
>
Simpan
</Button>
<Button
className="hover:bg-gray-300"
type="button" type="button"
variant="outline" variant="outline"
onClick={() => router.back()} onClick={() => router.back()}

View File

@ -58,6 +58,7 @@ export interface CreateArticleData {
tags: string; tags: string;
title: string; title: string;
typeId: number; typeId: number;
publishedFor: string;
} }
// Interface for Article Category // Interface for Article Category