Merge branch 'dev-1' of https://gitlab.com/hanifsalafi/new-netidhub-public
This commit is contained in:
commit
11d47e4c29
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,7 @@ type Option = {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FormVideo() {
|
export default function FormVideo() {
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const editor = useRef(null);
|
const editor = useRef(null);
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -352,7 +360,7 @@ export default function FormAudioUpdate() {
|
||||||
// isYoutube: false,
|
// isYoutube: false,
|
||||||
// isInternationalMedia: false,
|
// isInternationalMedia: false,
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
aiArticleId: detail.aiArticleId,
|
aiArticleId: detail.aiArticleId,
|
||||||
categoryIds: selectedCategory,
|
categoryIds: selectedCategory,
|
||||||
|
|
@ -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) => {
|
||||||
<div
|
if (file instanceof File) {
|
||||||
key={file.id} // Gunakan ID file sebagai key
|
return URL.createObjectURL(file);
|
||||||
className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
}
|
||||||
>
|
return file.secondaryUrl || file.fileUrl || null;
|
||||||
<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
|
const fileList = files.map((file: any) => {
|
||||||
type="button"
|
const audioSrc = getAudioUrl(file);
|
||||||
size="icon"
|
|
||||||
color="destructive"
|
return (
|
||||||
variant="outline"
|
<div
|
||||||
className="border-none rounded-full"
|
key={file.id || file.name}
|
||||||
onClick={() => handleRemoveFile(file)} // Kirim ID spesifik
|
className="flex flex-col gap-2 border p-3 my-6 rounded-md"
|
||||||
>
|
>
|
||||||
<Icon icon="tabler:x" className="h-5 w-5" />
|
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||||
</Button>
|
|
||||||
</div>
|
{audioSrc && (
|
||||||
));
|
<audio controls className="w-full">
|
||||||
|
<source src={audioSrc} type={file.type || "audio/mpeg"} />
|
||||||
|
Browser tidak mendukung audio.
|
||||||
|
</audio>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="self-end"
|
||||||
|
onClick={() => handleRemoveFile(file)}
|
||||||
|
>
|
||||||
|
<Icon icon="tabler:x" className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
|
@ -679,152 +702,142 @@ export default function FormTeksUpdate() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="py-3 space-y-2">
|
<div className="py-3 space-y-2">
|
||||||
<Label>Select File</Label>
|
|
||||||
{/* <Input
|
|
||||||
id="fileInput"
|
|
||||||
type="file"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
/> */}
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div {...getRootProps({ className: "dropzone" })}>
|
<div className="py-3 space-y-2">
|
||||||
<input {...getInputProps()} />
|
<Label>Select File</Label>
|
||||||
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
|
|
||||||
<CloudUpload className="text-default-300 w-10 h-10" />
|
<Fragment>
|
||||||
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
|
<div {...getRootProps({ className: "dropzone" })}>
|
||||||
{/* Drop files here or click to upload. */}
|
<input {...getInputProps()} />
|
||||||
Drag File
|
<div className="w-full text-center border-dashed border border-black rounded-md py-[52px] flex items-center flex-col">
|
||||||
</h4>
|
<CloudUpload className="w-10 h-10" />
|
||||||
<div className=" text-xs text-muted-foreground">
|
<h4 className="text-2xl font-medium mt-3">
|
||||||
Upload File Text Max
|
Drag File
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 👇 TARUH DI SINI */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<Label className="text-lg font-semibold">
|
||||||
|
File Baru
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{files.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.name}
|
||||||
|
className="flex justify-between items-center border p-3 rounded-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{file.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{(file.size / 1024).toFixed(1)} KB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setFiles((prev) =>
|
||||||
|
prev.filter(
|
||||||
|
(item) => item.name !== file.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* {files.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Label className="text-md font-semibold">
|
||||||
|
File Media
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{files.map((file: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={file.id}
|
||||||
|
className="flex items-center border p-2 rounded-md"
|
||||||
|
>
|
||||||
|
{file.preview ? (
|
||||||
|
<img
|
||||||
|
src={file.preview}
|
||||||
|
alt={file.name}
|
||||||
|
className="w-16 h-16 object-cover rounded-md mr-4"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
icon="tabler:file-description"
|
||||||
|
className="w-16 h-16"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-grow">
|
||||||
|
<p className="font-medium">
|
||||||
|
{file.fileName || file.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
setFiles((prev) =>
|
||||||
|
prev.filter((f) => f.id !== file.id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)} */}
|
||||||
{files.length ? (
|
|
||||||
<Fragment>
|
{/* Existing Files */}
|
||||||
<div>{fileList}</div>
|
{existingFiles.length > 0 && (
|
||||||
<div className=" flex justify-between gap-2">
|
<div className="mt-4 space-y-2">
|
||||||
{/* <div className="flex flex-row items-center gap-3 py-3">
|
<Label className="text-lg font-semibold">
|
||||||
<Label>Watermark</Label>
|
File Sebelumnya
|
||||||
<div className="flex items-center gap-3">
|
</Label>
|
||||||
<Switch defaultChecked color="primary" id="c2" />
|
|
||||||
</div>
|
{existingFiles.map((file) => (
|
||||||
</div> */}
|
<div
|
||||||
<Button
|
key={file.id}
|
||||||
color="destructive"
|
className="flex justify-between items-center border p-3 rounded-md"
|
||||||
onClick={handleRemoveAllFiles}
|
|
||||||
>
|
>
|
||||||
Remove All
|
<div className="flex items-center gap-3">
|
||||||
</Button>
|
<Icon
|
||||||
</div>
|
icon="tabler:file-description"
|
||||||
</Fragment>
|
className="w-10 h-10"
|
||||||
) : null}
|
/>
|
||||||
{files.length > 0 && (
|
<div>
|
||||||
<></>
|
<p className="font-medium">{file.fileName}</p>
|
||||||
// <div className="mt-4 space-y-2">
|
<a
|
||||||
// <Label className="text-lg font-semibold">
|
href={file.fileUrl}
|
||||||
// {" "}
|
target="_blank"
|
||||||
// File Media
|
rel="noopener noreferrer"
|
||||||
// </Label>
|
className="text-blue-500 text-sm"
|
||||||
// <div className="grid gap-4">
|
>
|
||||||
// {files.map((file: any) => (
|
Lihat File
|
||||||
// <div
|
</a>
|
||||||
// key={file.id}
|
</div>
|
||||||
// className="flex items-center border p-2 rounded-md"
|
</div>
|
||||||
// >
|
</div>
|
||||||
// <img
|
))}
|
||||||
// src={file.thumbnailFileUrl}
|
</div>
|
||||||
// alt={file.fileName}
|
|
||||||
// className="w-16 h-16 object-cover rounded-md mr-4"
|
|
||||||
// />
|
|
||||||
// <div className="flex flex-wrap gap-3 items-center ">
|
|
||||||
// <div className="flex-grow">
|
|
||||||
// <p className="font-medium">{file.fileName}</p>
|
|
||||||
// <a
|
|
||||||
// href={file.url}
|
|
||||||
// target="_blank"
|
|
||||||
// rel="noopener noreferrer"
|
|
||||||
// className="text-blue-500 text-sm"
|
|
||||||
// >
|
|
||||||
// View File
|
|
||||||
// </a>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Label className="flex items-center space-x-2">
|
|
||||||
// <input
|
|
||||||
// type="checkbox"
|
|
||||||
// checked={selectedOptions[
|
|
||||||
// file.id
|
|
||||||
// ]?.includes("all")}
|
|
||||||
// onChange={() =>
|
|
||||||
// handleCheckboxChangeImage(
|
|
||||||
// file.id,
|
|
||||||
// "all"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// className="form-checkbox"
|
|
||||||
// />
|
|
||||||
// <span>All</span>
|
|
||||||
// </Label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Label className="flex items-center space-x-2">
|
|
||||||
// <input
|
|
||||||
// type="checkbox"
|
|
||||||
// checked={selectedOptions[
|
|
||||||
// file.id
|
|
||||||
// ]?.includes("nasional")}
|
|
||||||
// onChange={() =>
|
|
||||||
// handleCheckboxChangeImage(
|
|
||||||
// file.id,
|
|
||||||
// "nasional"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// className="form-checkbox"
|
|
||||||
// />
|
|
||||||
// <span>Nasional</span>
|
|
||||||
// </Label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Label className="flex items-center space-x-2">
|
|
||||||
// <input
|
|
||||||
// type="checkbox"
|
|
||||||
// checked={selectedOptions[
|
|
||||||
// file.id
|
|
||||||
// ]?.includes("wilayah")}
|
|
||||||
// onChange={() =>
|
|
||||||
// handleCheckboxChangeImage(
|
|
||||||
// file.id,
|
|
||||||
// "wilayah"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// className="form-checkbox"
|
|
||||||
// />
|
|
||||||
// <span>Wilayah</span>
|
|
||||||
// </Label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Label className="flex items-center space-x-2">
|
|
||||||
// <input
|
|
||||||
// type="checkbox"
|
|
||||||
// checked={selectedOptions[
|
|
||||||
// file.id
|
|
||||||
// ]?.includes("internasional")}
|
|
||||||
// onChange={() =>
|
|
||||||
// handleCheckboxChangeImage(
|
|
||||||
// file.id,
|
|
||||||
// "internasional"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// className="form-checkbox"
|
|
||||||
// />
|
|
||||||
// <span>Internasional</span>
|
|
||||||
// </Label>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </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">
|
||||||
<img
|
{files.preview ? (
|
||||||
src={detail.thumbnailLink}
|
<img
|
||||||
alt="Thumbnail Gambar Utama"
|
src={files.preview}
|
||||||
className="w-full h-auto rounded"
|
alt={files.name}
|
||||||
/>
|
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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -1526,7 +1548,7 @@ export default function FormImage() {
|
||||||
option.id === "all"
|
option.id === "all"
|
||||||
? isAllChecked
|
? isAllChecked
|
||||||
: field.value.includes(option.id);
|
: field.value.includes(option.id);
|
||||||
|
|
||||||
const handleChange = (checked: boolean) => {
|
const handleChange = (checked: boolean) => {
|
||||||
let updated: string[] = [];
|
let updated: string[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue