From e60b45278006dbd04f2c83281deb489f77c19730 Mon Sep 17 00:00:00 2001 From: Sabda Yagra Date: Sat, 30 Aug 2025 09:28:28 +0700 Subject: [PATCH 01/11] fix: adjust broadcast admin --- .../account-list/component/column.tsx | 10 +- .../account-list/component/table.tsx | 284 ++++++++++++------ .../campaign-list/detail/[id]/page.tsx | 4 - app/[locale]/(public)/contact/page.tsx | 223 ++++++++++---- .../form/broadcast/content-blast-form.tsx | 2 +- service/broadcast/broadcast.ts | 19 +- 6 files changed, 379 insertions(+), 163 deletions(-) diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx index e21af944..515d48fc 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx @@ -30,34 +30,34 @@ const columns: ColumnDef[] = [ accessorKey: "accountName", header: "Nama", cell: ({ row }) => ( - {row.getValue("accountName")} + {row.original.mediaBlastAccount.accountName} ), }, { accessorKey: "accountType", header: "Tipe Akun", cell: ({ row }) => ( - {row.getValue("accountType")} + {row.original.mediaBlastAccount.accountType} ), }, { accessorKey: "accountCategory", header: "Kategory", cell: ({ row }) => ( - {row.getValue("accountCategory")} + {row.original.mediaBlastAccount.accountCategory} ), }, { accessorKey: "emailAddress", header: "Email", cell: ({ row }) => ( - {row.getValue("emailAddress")} + {row.original.mediaBlastAccount.emailAddress} ), }, { accessorKey: "whatsappNumber", header: "Whatsapp", - cell: ({ row }) => {row.getValue("whatsappNumber")}, + cell: ({ row }) => {row.original.mediaBlastAccount.whatsappNumber}, }, { id: "actions", diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx index c08b0a36..14c30f78 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { - ColumnDef, ColumnFiltersState, PaginationState, SortingState, @@ -15,7 +14,6 @@ import { useReactTable, } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; - import { Table, TableBody, @@ -24,25 +22,48 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { UserIcon } from "lucide-react"; - -import { useRouter, useSearchParams } from "next/navigation"; -import TablePagination from "@/components/table/table-pagination"; -import columns from "./column"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from "@/components/ui/dialog"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { getMediaBlastAccountPage } from "@/service/broadcast/broadcast"; import { Checkbox } from "@/components/ui/checkbox"; -import { close, loading } from "@/config/swal"; -import { Link } from "@/i18n/routing"; -import { Icon } from "@iconify/react/dist/iconify.js"; +import { Icon } from "@iconify/react"; +import { useParams, useSearchParams } from "next/navigation"; +import { UserIcon } from "lucide-react"; + +import columns from "./column"; +import TablePagination from "@/components/table/table-pagination"; +import { + getMediaBlastCampaignAccountList, + deleteMediaBlastCampaignAccount, + saveMediaBlastCampaignAccount, +} from "@/service/broadcast/broadcast"; +import { close, loading, error } from "@/config/swal"; const AccountListTable = () => { - const router = useRouter(); + const params = useParams(); const searchParams = useSearchParams(); + const campaignId = params?.id as string; + const [dataTable, setDataTable] = React.useState([]); const [totalData, setTotalData] = React.useState(1); const [sorting, setSorting] = React.useState([]); @@ -56,10 +77,15 @@ const AccountListTable = () => { pageIndex: 0, pageSize: 10, }); - const [page, setPage] = React.useState(1); const [totalPage, setTotalPage] = React.useState(1); const [filtered, setFiltered] = React.useState([]); + + // --- state utk Dialog Pilih Akun --- + const [accountCategory, setAccountCategory] = React.useState(""); + const [selectedAccount, setSelectedAccount] = React.useState([]); + const [selectedCategory, setSelectedCategory] = React.useState(""); + const table = useReactTable({ data: dataTable, columns, @@ -83,24 +109,24 @@ const AccountListTable = () => { React.useEffect(() => { const pageFromUrl = searchParams?.get("page"); - if (pageFromUrl) { - setPage(Number(pageFromUrl)); - } + if (pageFromUrl) setPage(Number(pageFromUrl)); }, [searchParams]); React.useEffect(() => { fetchData(); - }, [page]); + }, [page, filtered]); async function fetchData() { try { loading(); - const res = await getMediaBlastAccountPage( + const res = await getMediaBlastCampaignAccountList( page - 1, - filtered ? filtered.join(",") : "" + filtered ? filtered.join(",") : "", + campaignId ); + const data = res?.data?.data; - const contentData = data?.content; + const contentData = data?.content || []; contentData.forEach((item: any, index: number) => { item.no = (page - 1) * 10 + index + 1; }); @@ -109,20 +135,43 @@ const AccountListTable = () => { setTotalData(data?.totalElements); setTotalPage(data?.totalPages); close(); - } catch (error) { - console.error("Error fetching tasks:", error); + } catch (err) { + console.error("Error fetching tasks:", err); + close(); } } + // --- API helpers --- + async function doDeleteAccount(id: string) { + loading(); + const response = await deleteMediaBlastCampaignAccount(id); + close(); + if (response?.error) { + error(response.message); + return; + } + fetchData(); + } + + async function saveCampaignAccount() { + for (const acc of selectedAccount) { + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + }; + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + } + } + fetchData(); + } + const handleFilter = (id: string, checked: boolean) => { let temp = [...filtered]; - if (checked) { - temp = [...temp, id]; - } else { - temp = temp.filter((a) => a !== id); - } + if (checked) temp = [...temp, id]; + else temp = temp.filter((a) => a !== id); setFiltered(temp); - console.log("sss", temp); }; return ( @@ -130,20 +179,103 @@ const AccountListTable = () => {

Daftar Akun

- - - - {/* - - */} + {/* === Dialog Pilih Akun === */} + + + + + + + Pilih Akun Untuk Campaign Ini + + + setAccountCategory(val)} + className="space-y-3" + > +
+ + +
+
+ + +
+
+ + +
+
+ +
+ {accountCategory === "custom" && ( + <> + + {selectedAccount.length < 1 && ( +

+ Pilih minimal 1 akun +

+ )} + + )} + + {accountCategory === "kategori" && ( + + )} + + {accountCategory === "all-account" && ( +

Semua akun dipilih

+ )} +
+ + + + + + + +
+
+ + {/* === Filter Akun === */}
@@ -163,65 +295,28 @@ const AccountListTable = () => {
-
- handleFilter("polri", Boolean(e))} - /> - -
-
- - handleFilter("jurnalis", Boolean(e)) - } - /> - -
-
- handleFilter("umum", Boolean(e))} - /> - -
-
- handleFilter("ksp", Boolean(e))} - /> - -
+ {["polri", "jurnalis", "umum", "ksp"].map((cat) => ( +
+ handleFilter(cat, Boolean(e))} + /> + +
+ ))}
+ + {/* === Table Data === */} {table.getHeaderGroups().map((headerGroup) => ( @@ -263,6 +358,7 @@ const AccountListTable = () => { )}
+ ("sent"); const { page, size } = searchParams; - const [calenderState, setCalenderState] = useState(false); const [typeFilter, setTypeFilter] = useState("email"); const [dateRange, setDateRange] = useState<[Date, Date]>([ @@ -91,10 +90,8 @@ export default function BroadcastCampaignDetail({ new Date(), ]); const [startDate, endDate] = dateRange; - const [startDateString, setStartDateString] = useState(); const [endDateString, setEndDateString] = useState(); - // Table state const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); @@ -104,7 +101,6 @@ export default function BroadcastCampaignDetail({ pageIndex: 0, pageSize: parseInt(size || "10"), }); - const pages = page ? parseInt(page) - 1 : 0; const currentPage = page ? parseInt(page) : 1; const pageSize = parseInt(size || "10"); diff --git a/app/[locale]/(public)/contact/page.tsx b/app/[locale]/(public)/contact/page.tsx index 710da1bf..601c7336 100644 --- a/app/[locale]/(public)/contact/page.tsx +++ b/app/[locale]/(public)/contact/page.tsx @@ -1,59 +1,73 @@ "use client"; import { Reveal } from "@/components/landing-page/Reveal"; import { getCookiesDecrypt } from "@/lib/utils"; -import { useRouter } from "next/navigation"; import React, { useEffect, useState } from "react"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as Yup from "yup"; import { useForm } from "react-hook-form"; -import { getInfoProfile, getProfile, getSubjects } from "@/service/auth"; +import { getInfoProfile, getSubjects } from "@/service/auth"; import { close, error, loading, successCallback } from "@/config/swal"; import { sendMessage } from "@/service/landing/landing"; import { useTranslations } from "next-intl"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "@/i18n/routing"; + +interface IFormInput { + name: string; + email: string; + phone?: string | undefined; + subjects: string; + othersubject?: string | undefined; + message: string; +} const ContactForm = () => { const router = useRouter(); const userId = getCookiesDecrypt("uie"); - const [subjects, setSubjects] = useState(); + const [subjects, setSubjects] = useState([]); const [isOtherActive, setIsOtherActive] = useState(false); const t = useTranslations("LandingPage"); - const form = document.getElementById("form") as HTMLFormElement; - - // Form Handling - const validationSchema = Yup.object().shape({ - name: Yup.string().required("Nama tidak boleh kosong"), - email: Yup.string().required("Email tidak boleh kosong"), - subjects: Yup.string().required("Subjek tidak boleh kosong"), - message: Yup.string().required("Pesan tidak boleh kosong"), + const validationSchema = z.object({ + name: z.string().min(1, "Nama tidak boleh kosong"), + email: z.string().email("Email tidak valid"), + phone: z.string().optional(), + subjects: z.string().min(1, "Subjek tidak boleh kosong"), + othersubject: z.string().optional(), + message: z.string().min(1, "Pesan tidak boleh kosong"), }); - const formOptions = { - resolver: yupResolver(validationSchema), - }; + type IFormInput = z.infer; - const { register, handleSubmit, formState, setValue } = useForm(formOptions); - - const { errors } = formState; + const { + register, + handleSubmit, + formState: { errors }, + setValue, + reset, + } = useForm({ + resolver: zodResolver(validationSchema), + }); + // Init state useEffect(() => { async function initState() { const response = await getInfoProfile(); const responseSubject = await getSubjects(); const profile = response?.data?.data; - setSubjects(responseSubject?.data?.data); - console.log(response); - setValue("name", profile?.fullname); - setValue("email", profile?.email); // setValue('name', profile?.fullname); - // setValue('name', profile?.fullname); + setSubjects(responseSubject?.data?.data || []); + if (profile) { + setValue("name", profile?.fullname || ""); + setValue("email", profile?.email || ""); + } } initState(); - }, []); + }, [setValue]); - async function save(data: any) { + async function save(data: IFormInput) { loading(); + const finalData = { name: data.name, email: data.email, @@ -65,29 +79,24 @@ const ContactForm = () => { const response = await sendMessage(finalData); if (response?.error) { error(response?.message); - return false; + return; } close(); successCallback("Terima kasih, pesan Anda telah terkirim"); - // $("#form")[0].onreset(); - if (form) { - form.reset(); - } + reset(); } - async function onSubmit(data: any) { + async function onSubmit(data: IFormInput) { if (userId == undefined) { - router.push("/auth/login"); + router.push("/auth"); } else { save(data); } } - const handleSubjects = (e: any) => { - const id = e.target.value; - - if (id == "Lainnya") { + const handleSubjects = (e: React.ChangeEvent) => { + if (e.target.value === "Lainnya") { setIsOtherActive(true); } else { setIsOtherActive(false); @@ -95,63 +104,163 @@ const ContactForm = () => { }; return ( -
+ {/* Header */}
contact -

{t("contactUs", { defaultValue: "Contact Us" })}

+

+ {t("contactUs", { defaultValue: "Contact Us" })} +

-

{t("writeMessage", { defaultValue: "Write Message" })}

-

{t("leaveMessage", { defaultValue: "Leave Message" })}

+

+ {t("writeMessage", { defaultValue: "Write Message" })} +

+

+ {t("leaveMessage", { defaultValue: "Leave Message" })} +

{/* Form */} - +
+ {/* Name */}
- - + + + {errors.name && ( +

{errors.name.message}

+ )}
+ {/* Email */}
- - + + + {errors.email && ( +

{errors.email.message}

+ )}
+ {/* Phone */}
- - + +
+ {/* Subjects */}
- + + {errors.subjects && ( +

{errors.subjects.message}

+ )}
+ {/* Other Subject */} + {isOtherActive && ( +
+ + + {errors.othersubject && ( +

+ {errors.othersubject.message} +

+ )} +
+ )} + + {/* Message */}
- - + + + {errors.message && ( +

{errors.message.message}

+ )}
- - +
); diff --git a/components/form/broadcast/content-blast-form.tsx b/components/form/broadcast/content-blast-form.tsx index 8fb97ded..1997b475 100644 --- a/components/form/broadcast/content-blast-form.tsx +++ b/components/form/broadcast/content-blast-form.tsx @@ -290,7 +290,7 @@ export default function ContentBlast(props: { type: string }) { - Email Terkirim + Terkirim !!
Date: Mon, 1 Sep 2025 02:06:19 +0700 Subject: [PATCH 02/11] feat: update ckeditor --- .../account-list/component/table.tsx | 260 +++++++++++++----- app/[locale]/globals.css | 98 +++++++ app/[locale]/layout.tsx | 1 + components/editor/custom-editor.js | 140 ++++++++-- components/editor/view-editor.js | 111 +++++++- components/form/content/image-detail-form.tsx | 4 +- .../content/task-ta/audio-detail-form.tsx | 4 +- .../content/task-ta/image-detail-form.tsx | 4 +- .../form/content/task-ta/teks-detail-form.tsx | 4 +- .../content/task-ta/video-detail-form.tsx | 4 +- components/form/content/teks-detail-form.tsx | 4 +- components/form/content/video-detail-form.tsx | 2 +- style/ckeditor.css | 215 +++++++++++++++ 13 files changed, 733 insertions(+), 118 deletions(-) create mode 100644 style/ckeditor.css diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx index 14c30f78..c2388f79 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx @@ -48,7 +48,8 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Icon } from "@iconify/react"; import { useParams, useSearchParams } from "next/navigation"; -import { UserIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { X } from "lucide-react"; import columns from "./column"; import TablePagination from "@/components/table/table-pagination"; @@ -57,7 +58,16 @@ import { deleteMediaBlastCampaignAccount, saveMediaBlastCampaignAccount, } from "@/service/broadcast/broadcast"; -import { close, loading, error } from "@/config/swal"; +import { close, loading, error, success } from "@/config/swal"; + +// Mock data for available accounts - replace with actual API call +const availableAccounts = [ + { id: "1", accountName: "Account 1", category: "polri" }, + { id: "2", accountName: "Account 2", category: "jurnalis" }, + { id: "3", accountName: "Account 3", category: "umum" }, + { id: "4", accountName: "Account 4", category: "ksp" }, + { id: "5", accountName: "Account 5", category: "polri" }, +]; const AccountListTable = () => { const params = useParams(); @@ -82,9 +92,11 @@ const AccountListTable = () => { const [filtered, setFiltered] = React.useState([]); // --- state utk Dialog Pilih Akun --- + const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [accountCategory, setAccountCategory] = React.useState(""); const [selectedAccount, setSelectedAccount] = React.useState([]); const [selectedCategory, setSelectedCategory] = React.useState(""); + const [availableAccountsList, setAvailableAccountsList] = React.useState(availableAccounts); const table = useReactTable({ data: dataTable, @@ -154,19 +166,72 @@ const AccountListTable = () => { } async function saveCampaignAccount() { - for (const acc of selectedAccount) { - const request = { - mediaBlastCampaignId: campaignId, - mediaBlastAccountId: acc.id, - }; - const response = await saveMediaBlastCampaignAccount(request); - if (response?.error) { - error(response.message); + try { + loading(); + + if (accountCategory === "all-account") { + // Handle all accounts selection + const allAccounts = availableAccountsList.map(acc => ({ + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + })); + + for (const request of allAccounts) { + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } + } else if (accountCategory === "kategori" && selectedCategory) { + // Handle category selection + const categoryAccounts = availableAccountsList.filter( + acc => acc.category === selectedCategory + ); + + for (const acc of categoryAccounts) { + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + }; + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } + } else if (accountCategory === "custom") { + // Handle custom selection + for (const acc of selectedAccount) { + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + }; + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } } + + close(); + success("Akun berhasil ditambahkan ke campaign!"); + resetDialogState(); + fetchData(); + } catch (err) { + close(); + error("Terjadi kesalahan saat menyimpan akun"); } - fetchData(); } + const resetDialogState = () => { + setAccountCategory(""); + setSelectedAccount([]); + setSelectedCategory(""); + setIsDialogOpen(false); + }; + const handleFilter = (id: string, checked: boolean) => { let temp = [...filtered]; if (checked) temp = [...temp, id]; @@ -174,86 +239,143 @@ const AccountListTable = () => { setFiltered(temp); }; + const handleAccountSelection = (accountId: string, checked: boolean) => { + if (checked) { + const account = availableAccountsList.find(acc => acc.id === accountId); + if (account && !selectedAccount.find(acc => acc.id === accountId)) { + setSelectedAccount([...selectedAccount, account]); + } + } else { + setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); + } + }; + + const removeSelectedAccount = (accountId: string) => { + setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); + }; + + const getFilteredAccounts = () => { + if (accountCategory === "kategori" && selectedCategory) { + return availableAccountsList.filter(acc => acc.category === selectedCategory); + } + return availableAccountsList; + }; + return (

Daftar Akun

{/* === Dialog Pilih Akun === */} - + - + Pilih Akun Untuk Campaign Ini - setAccountCategory(val)} - className="space-y-3" - > -
- - -
-
- - -
-
- - -
-
+
+ { + setAccountCategory(val); + setSelectedAccount([]); + setSelectedCategory(""); + }} + className="space-y-3" + > +
+ + +
+
+ + +
+
+ + +
+
-
- {accountCategory === "custom" && ( - <> - setSelectedCategory(val)}> - + - {dataTable.map((acc) => ( - - {acc.accountName} - - ))} + Umum + Polri + KSP + Jurnalis - {selectedAccount.length < 1 && ( -

- Pilih minimal 1 akun -

+
+ )} + + {/* Custom Account Selection */} + {accountCategory === "custom" && ( +
+ +
+ {getFilteredAccounts().map((acc) => ( +
+ selected.id === acc.id)} + onCheckedChange={(checked) => handleAccountSelection(acc.id, Boolean(checked))} + /> + +
+ ))} +
+ + {/* Selected Accounts Display */} + {selectedAccount.length > 0 && ( +
+ +
+ {selectedAccount.map((acc) => ( + + {acc.accountName} + removeSelectedAccount(acc.id)} + /> + + ))} +
+
)} - - )} - - {accountCategory === "kategori" && ( - +
)} + {/* All Accounts Info */} {accountCategory === "all-account" && ( -

Semua akun dipilih

+
+

+ Semua akun ({availableAccountsList.length} akun) akan ditambahkan ke campaign ini. +

+
+ )} + + {/* Category Accounts Info */} + {accountCategory === "kategori" && selectedCategory && ( +
+

+ {getFilteredAccounts().length} akun dari kategori "{selectedCategory}" akan ditambahkan. +

+
)}
@@ -261,13 +383,17 @@ const AccountListTable = () => { - +
diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index 688a4f26..25750c7f 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -575,10 +575,108 @@ html[dir="rtl"] .react-select .select__loading-indicator { background: #9ca3af; } +/* CKEditor Styling */ .ck-editor__editable_inline { min-height: 200px; } +/* Main CKEditor content area styling */ +.ck.ck-editor__editable { + padding: 1.5em 2em !important; + min-height: 400px; + max-height: 600px; + line-height: 1.6; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 #f1f5f9; +} + +/* CKEditor content styling */ +.ck.ck-editor__editable .ck-content { + padding: 0; +} + +/* CKEditor scrollbar styling */ +.ck.ck-editor__editable::-webkit-scrollbar { + width: 8px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* CKEditor editable area focus state */ +.ck.ck-editor__editable.ck-focused { + border-color: #1a9aef; + box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2); +} + +/* CKEditor toolbar styling */ +.ck.ck-toolbar { + border-radius: 4px 4px 0 0; +} + +/* CKEditor editable border styling */ +.ck.ck-editor__editable { + border-radius: 0 0 4px 4px; + border: 1px solid #d1d5db; +} + +/* CKEditor content typography */ +.ck.ck-editor__editable p { + margin: 0.5em 0; +} + +/* View Editor specific styling (read-only mode) */ +.ckeditor-view-wrapper .ck.ck-editor__editable { + background-color: #f8fafc !important; + color: #4b5563 !important; + cursor: default !important; + border: 1px solid #d1d5db !important; + border-radius: 6px !important; +} + +.ckeditor-view-wrapper .ck.ck-editor__editable.ck-focused { + border-color: #d1d5db !important; + box-shadow: none !important; +} + +.ckeditor-view-wrapper .ck.ck-toolbar { + display: none !important; +} + +.ck.ck-editor__editable h1, +.ck.ck-editor__editable h2, +.ck.ck-editor__editable h3, +.ck.ck-editor__editable h4, +.ck.ck-editor__editable h5, +.ck.ck-editor__editable h6 { + margin: 1em 0 0.5em 0; +} + +.ck.ck-editor__editable ul, +.ck.ck-editor__editable ol { + margin: 0.5em 0; + padding-left: 2em; +} + +.ck.ck-editor__editable blockquote { + margin: 1em 0; + padding: 0.5em 1em; + border-left: 4px solid #d1d5db; + background-color: #f9fafb; +} + /* Hide FullCalendar grid elements */ .fc-view-harness:has(.hide-calendar-grid) { display: none; diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 67ff5946..3204db83 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import "./theme.css"; +import "../../style/ckeditor.css"; import { ThemeProvider } from "@/providers/theme-provider"; import MountedProvider from "@/providers/mounted.provider"; import { Toaster } from "@/components/ui/toaster"; diff --git a/components/editor/custom-editor.js b/components/editor/custom-editor.js index f28746b2..a3f7ed77 100644 --- a/components/editor/custom-editor.js +++ b/components/editor/custom-editor.js @@ -5,36 +5,118 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "ckeditor5-custom-build"; function CustomEditor(props) { + const maxHeight = props.maxHeight || 600; // Default max height 600px + return ( - { - const data = editor.getData(); - console.log({ event, editor, data }); - props.onChange(data); - }} - config={{ - toolbar: [ - "heading", - "fontsize", - "bold", - "italic", - "link", - "numberedList", - "bulletedList", - "undo", - "redo", - "alignment", - "outdent", - "indent", - "blockQuote", - "insertTable", - "codeBlock", - "sourceEditing", - ], - }} - /> +
+ { + const data = editor.getData(); + console.log({ event, editor, data }); + props.onChange(data); + }} + config={{ + toolbar: [ + "heading", + "fontsize", + "bold", + "italic", + "link", + "numberedList", + "bulletedList", + "undo", + "redo", + "alignment", + "outdent", + "indent", + "blockQuote", + "insertTable", + "codeBlock", + "sourceEditing", + ], + // Add content styling configuration + content_style: ` + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.6; + color: #333; + margin: 0; + padding: 0; + } + p { + margin: 0.5em 0; + } + h1, h2, h3, h4, h5, h6 { + margin: 1em 0 0.5em 0; + } + ul, ol { + margin: 0.5em 0; + padding-left: 2em; + } + blockquote { + margin: 1em 0; + padding: 0.5em 1em; + border-left: 4px solid #d1d5db; + background-color: #f9fafb; + } + `, + // Editor appearance settings + height: props.height || 400, + removePlugins: ['Title'], + // Better mobile support + mobile: { + theme: 'silver' + } + }} + /> + +
); } diff --git a/components/editor/view-editor.js b/components/editor/view-editor.js index c2529003..aee35c00 100644 --- a/components/editor/view-editor.js +++ b/components/editor/view-editor.js @@ -3,16 +3,109 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "ckeditor5-custom-build"; function ViewEditor(props) { + const maxHeight = props.maxHeight || 600; // Default max height 600px + return ( - +
+ + +
); } diff --git a/components/form/content/image-detail-form.tsx b/components/form/content/image-detail-form.tsx index 037f2836..7f2f620f 100644 --- a/components/form/content/image-detail-form.tsx +++ b/components/form/content/image-detail-form.tsx @@ -950,8 +950,8 @@ export default function FormImageDetail() { setCheckedLevels(levels); } - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/audio-detail-form.tsx b/components/form/content/task-ta/audio-detail-form.tsx index f8470870..46534ac9 100644 --- a/components/form/content/task-ta/audio-detail-form.tsx +++ b/components/form/content/task-ta/audio-detail-form.tsx @@ -261,8 +261,8 @@ export default function FormAudioTaDetail() { }); setupPlacementCheck(details?.files?.length); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/image-detail-form.tsx b/components/form/content/task-ta/image-detail-form.tsx index 92280fcf..70f4fc1d 100644 --- a/components/form/content/task-ta/image-detail-form.tsx +++ b/components/form/content/task-ta/image-detail-form.tsx @@ -247,8 +247,8 @@ export default function FormImageTaDetail() { }); setupPlacementCheck(details?.files?.length); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/teks-detail-form.tsx b/components/form/content/task-ta/teks-detail-form.tsx index 0bb0cc98..72d9ada0 100644 --- a/components/form/content/task-ta/teks-detail-form.tsx +++ b/components/form/content/task-ta/teks-detail-form.tsx @@ -246,8 +246,8 @@ export default function FormTeksTaDetail() { format: details?.files[0]?.format, }); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/video-detail-form.tsx b/components/form/content/task-ta/video-detail-form.tsx index 6c201bc7..c3b9ce82 100644 --- a/components/form/content/task-ta/video-detail-form.tsx +++ b/components/form/content/task-ta/video-detail-form.tsx @@ -237,8 +237,8 @@ export default function FormVideoTaDetail() { format: details?.files[0]?.format, }); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/teks-detail-form.tsx b/components/form/content/teks-detail-form.tsx index 3f2659b9..c54a94e1 100644 --- a/components/form/content/teks-detail-form.tsx +++ b/components/form/content/teks-detail-form.tsx @@ -519,8 +519,8 @@ export default function FormTeksDetail() { setCheckedLevels(levels); } - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/video-detail-form.tsx b/components/form/content/video-detail-form.tsx index 857eec2f..048d53d3 100644 --- a/components/form/content/video-detail-form.tsx +++ b/components/form/content/video-detail-form.tsx @@ -492,7 +492,7 @@ export default function FormVideoDetail() { } if (details?.publishedForObject) { - const publisherIds = details.publishedForObject.map( + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/style/ckeditor.css b/style/ckeditor.css new file mode 100644 index 00000000..e55d0761 --- /dev/null +++ b/style/ckeditor.css @@ -0,0 +1,215 @@ +/* CKEditor Custom Styling */ + +/* Main editor container */ +.ck.ck-editor { + border-radius: 6px; + overflow: hidden; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +/* Toolbar styling */ +.ck.ck-toolbar { + background: #f8fafc; + border: 1px solid #d1d5db; + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 8px; +} + +.ck.ck-toolbar .ck-toolbar__items { + gap: 4px; +} + +/* Main editable area */ +.ck.ck-editor__editable { + background: #ffffff; + border: 1px solid #d1d5db; + border-top: none; + border-radius: 0 0 6px 6px; + padding: 1.5em 2em !important; + min-height: 400px; + line-height: 1.6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #333; +} + +/* Focus state */ +.ck.ck-editor__editable.ck-focused { + border-color: #1a9aef; + box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2); + outline: none; +} + +/* Content styling */ +.ck.ck-editor__editable .ck-content { + padding: 0; +} + +/* Typography improvements */ +.ck.ck-editor__editable p { + margin: 0.5em 0; + line-height: 1.6; +} + +.ck.ck-editor__editable h1, +.ck.ck-editor__editable h2, +.ck.ck-editor__editable h3, +.ck.ck-editor__editable h4, +.ck.ck-editor__editable h5, +.ck.ck-editor__editable h6 { + margin: 1em 0 0.5em 0; + font-weight: 600; + line-height: 1.4; +} + +.ck.ck-editor__editable h1 { font-size: 1.75em; } +.ck.ck-editor__editable h2 { font-size: 1.5em; } +.ck.ck-editor__editable h3 { font-size: 1.25em; } +.ck.ck-editor__editable h4 { font-size: 1.1em; } +.ck.ck-editor__editable h5 { font-size: 1em; } +.ck.ck-editor__editable h6 { font-size: 0.9em; } + +/* Lists */ +.ck.ck-editor__editable ul, +.ck.ck-editor__editable ol { + margin: 0.5em 0; + padding-left: 2em; +} + +.ck.ck-editor__editable li { + margin: 0.25em 0; + line-height: 1.6; +} + +/* Blockquotes */ +.ck.ck-editor__editable blockquote { + margin: 1em 0; + padding: 0.75em 1em; + border-left: 4px solid #1a9aef; + background-color: #f8fafc; + border-radius: 0 4px 4px 0; + font-style: italic; + color: #4b5563; +} + +/* Tables */ +.ck.ck-editor__editable table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; +} + +.ck.ck-editor__editable table td, +.ck.ck-editor__editable table th { + border: 1px solid #d1d5db; + padding: 0.5em 0.75em; + text-align: left; +} + +.ck.ck-editor__editable table th { + background-color: #f8fafc; + font-weight: 600; +} + +/* Links */ +.ck.ck-editor__editable a { + color: #1a9aef; + text-decoration: underline; +} + +.ck.ck-editor__editable a:hover { + color: #0d7cd6; +} + +/* Code blocks */ +.ck.ck-editor__editable pre { + background-color: #f8fafc; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 1em; + margin: 1em 0; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; +} + +.ck.ck-editor__editable code { + background-color: #f1f5f9; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +/* Images */ +.ck.ck-editor__editable img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 0.5em 0; +} + +/* Horizontal rule */ +.ck.ck-editor__editable hr { + border: none; + border-top: 1px solid #d1d5db; + margin: 2em 0; +} + +/* Placeholder text */ +.ck.ck-editor__editable.ck-blurred:empty::before { + content: attr(data-placeholder); + color: #9ca3af; + font-style: italic; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .ck.ck-editor__editable { + padding: 1em 1.5em !important; + font-size: 16px; /* Better for mobile */ + } + + .ck.ck-toolbar { + padding: 6px; + } + + .ck.ck-toolbar .ck-toolbar__items { + gap: 2px; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .ck.ck-editor__editable { + background: #1f2937; + color: #f9fafb; + border-color: #4b5563; + } + + .ck.ck-editor__editable h1, + .ck.ck-editor__editable h2, + .ck.ck-editor__editable h3, + .ck.ck-editor__editable h4, + .ck.ck-editor__editable h5, + .ck.ck-editor__editable h6 { + color: #f9fafb; + } + + .ck.ck-editor__editable blockquote { + background-color: #374151; + border-left-color: #1a9aef; + color: #d1d5db; + } + + .ck.ck-editor__editable pre { + background-color: #374151; + border-color: #4b5563; + } + + .ck.ck-editor__editable code { + background-color: #4b5563; + } +} From 3fee36390d57c5b62c5423cae23f1e6fcdd6f804 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Mon, 1 Sep 2025 03:30:13 +0700 Subject: [PATCH 03/11] feat: update media broadcast, update blogs --- .../account-list/component/column.tsx | 15 +- .../account-list/component/table.tsx | 200 +++++++++++------- components/form/blog/blog-form.tsx | 4 + service/broadcast/broadcast.ts | 6 +- 4 files changed, 134 insertions(+), 91 deletions(-) diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx index 515d48fc..f8dcfd54 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/column.tsx @@ -30,34 +30,27 @@ const columns: ColumnDef[] = [ accessorKey: "accountName", header: "Nama", cell: ({ row }) => ( - {row.original.mediaBlastAccount.accountName} + {row.original.mediaBlastAccountName} ), }, { accessorKey: "accountType", header: "Tipe Akun", cell: ({ row }) => ( - {row.original.mediaBlastAccount.accountType} - ), - }, - { - accessorKey: "accountCategory", - header: "Kategory", - cell: ({ row }) => ( - {row.original.mediaBlastAccount.accountCategory} + {row.original.mediaBlastAccountType} ), }, { accessorKey: "emailAddress", header: "Email", cell: ({ row }) => ( - {row.original.mediaBlastAccount.emailAddress} + {row.original.mediaBlastAccountEmail} ), }, { accessorKey: "whatsappNumber", header: "Whatsapp", - cell: ({ row }) => {row.original.mediaBlastAccount.whatsappNumber}, + cell: ({ row }) => {row.original.mediaBlastAccountPhone}, }, { id: "actions", diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx index c2388f79..f7d3bc28 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx @@ -34,7 +34,7 @@ import { import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Label } from "@/components/ui/label"; import { - Select, + Select as UISelect, SelectContent, SelectItem, SelectTrigger, @@ -50,15 +50,17 @@ import { Icon } from "@iconify/react"; import { useParams, useSearchParams } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { X } from "lucide-react"; +import ReactSelect from "react-select"; import columns from "./column"; import TablePagination from "@/components/table/table-pagination"; import { getMediaBlastCampaignAccountList, deleteMediaBlastCampaignAccount, - saveMediaBlastCampaignAccount, + saveMediaBlastCampaignAccountBulk, } from "@/service/broadcast/broadcast"; -import { close, loading, error, success } from "@/config/swal"; +import { AdministrationUserList } from "@/service/management-user/management-user"; +import { close, loading, error, success, successCallback } from "@/config/swal"; // Mock data for available accounts - replace with actual API call const availableAccounts = [ @@ -97,6 +99,7 @@ const AccountListTable = () => { const [selectedAccount, setSelectedAccount] = React.useState([]); const [selectedCategory, setSelectedCategory] = React.useState(""); const [availableAccountsList, setAvailableAccountsList] = React.useState(availableAccounts); + const [usersList, setUsersList] = React.useState([]); const table = useReactTable({ data: dataTable, @@ -170,53 +173,60 @@ const AccountListTable = () => { loading(); if (accountCategory === "all-account") { - // Handle all accounts selection - const allAccounts = availableAccountsList.map(acc => ({ + // Handle all accounts - send only campaignId and category "all" + const request = { mediaBlastCampaignId: campaignId, - mediaBlastAccountId: acc.id, - })); - - for (const request of allAccounts) { - const response = await saveMediaBlastCampaignAccount(request); - if (response?.error) { - error(response.message); - return; - } + mediaBlastAccountCategory: "all", + }; + const response = await saveMediaBlastCampaignAccountBulk(request); + if (response?.error) { + error(response.message); + return; } } else if (accountCategory === "kategori" && selectedCategory) { - // Handle category selection - const categoryAccounts = availableAccountsList.filter( - acc => acc.category === selectedCategory - ); + // Handle category selection - send campaignId and role-based category + let roleId = ""; + switch (selectedCategory) { + case "umum": + roleId = "5"; + break; + case "jurnalis": + roleId = "6"; + break; + case "polri": + roleId = "7"; + break; + case "ksp": + roleId = "8"; + break; + default: + roleId = "5"; + } - for (const acc of categoryAccounts) { - const request = { - mediaBlastCampaignId: campaignId, - mediaBlastAccountId: acc.id, - }; - const response = await saveMediaBlastCampaignAccount(request); - if (response?.error) { - error(response.message); - return; - } + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountCategory: `role-${roleId}`, + }; + const response = await saveMediaBlastCampaignAccountBulk(request); + if (response?.error) { + error(response.message); + return; } } else if (accountCategory === "custom") { - // Handle custom selection - for (const acc of selectedAccount) { - const request = { - mediaBlastCampaignId: campaignId, - mediaBlastAccountId: acc.id, - }; - const response = await saveMediaBlastCampaignAccount(request); - if (response?.error) { - error(response.message); - return; - } + // Handle custom selection - send campaignId and selected user IDs + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountIds: selectedAccount.map(acc => acc.id), + }; + const response = await saveMediaBlastCampaignAccountBulk(request); + if (response?.error) { + error(response.message); + return; } } - + close(); - success("Akun berhasil ditambahkan ke campaign!"); + successCallback("Akun berhasil ditambahkan ke campaign!"); resetDialogState(); fetchData(); } catch (err) { @@ -229,9 +239,32 @@ const AccountListTable = () => { setAccountCategory(""); setSelectedAccount([]); setSelectedCategory(""); + setUsersList([]); setIsDialogOpen(false); }; + const fetchUsersList = async () => { + try { + loading(); + const response = await AdministrationUserList( + "1", // levelId + 0, // page + "", // name + "100", // size + "1", // featureId + "" // role + ); + + if (response?.data?.data?.content) { + setUsersList(response.data.data.content); + } + close(); + } catch (err) { + close(); + error("Terjadi kesalahan saat mengambil daftar user"); + } + }; + const handleFilter = (id: string, checked: boolean) => { let temp = [...filtered]; if (checked) temp = [...temp, id]; @@ -239,16 +272,7 @@ const AccountListTable = () => { setFiltered(temp); }; - const handleAccountSelection = (accountId: string, checked: boolean) => { - if (checked) { - const account = availableAccountsList.find(acc => acc.id === accountId); - if (account && !selectedAccount.find(acc => acc.id === accountId)) { - setSelectedAccount([...selectedAccount, account]); - } - } else { - setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); - } - }; + const removeSelectedAccount = (accountId: string) => { setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); @@ -274,20 +298,23 @@ const AccountListTable = () => { Pilih Akun - + Pilih Akun Untuk Campaign Ini -
+
{ setAccountCategory(val); setSelectedAccount([]); setSelectedCategory(""); + if (val === "custom") { + fetchUsersList(); + } }} - className="space-y-3" + className="flex space-x-6" >
@@ -299,7 +326,7 @@ const AccountListTable = () => {
- +
@@ -307,7 +334,7 @@ const AccountListTable = () => { {accountCategory === "kategori" && (
- +
)} {/* Custom Account Selection */} {accountCategory === "custom" && (
- -
- {getFilteredAccounts().map((acc) => ( -
- selected.id === acc.id)} - onCheckedChange={(checked) => handleAccountSelection(acc.id, Boolean(checked))} - /> - -
- ))} -
+ + ({ + value: user.id, + label: `${user.fullname} (${user.role?.name})`, + user: user + }))} + value={selectedAccount.map((acc: any) => ({ + value: acc.id, + label: `${acc.fullname} (${acc.role?.name})`, + user: acc + }))} + onChange={(selectedOptions: any) => { + const selectedUsers = selectedOptions ? selectedOptions.map((option: any) => option.user) : []; + setSelectedAccount(selectedUsers); + }} + placeholder="Cari dan pilih user..." + noOptionsMessage={() => "Tidak ada user ditemukan"} + loadingMessage={() => "Memuat..."} + isSearchable={true} + isClearable={true} + className="react-select" + classNamePrefix="select" + /> {/* Selected Accounts Display */} {selectedAccount.length > 0 && (
- +
{selectedAccount.map((acc) => ( - - {acc.accountName} + + {acc.fullname} removeSelectedAccount(acc.id)} @@ -364,7 +401,7 @@ const AccountListTable = () => { {accountCategory === "all-account" && (

- Semua akun ({availableAccountsList.length} akun) akan ditambahkan ke campaign ini. + Semua akun akan ditambahkan ke campaign ini.

)} @@ -373,7 +410,16 @@ const AccountListTable = () => { {accountCategory === "kategori" && selectedCategory && (

- {getFilteredAccounts().length} akun dari kategori "{selectedCategory}" akan ditambahkan. + Semua akun dengan role "{selectedCategory.toUpperCase()}" akan ditambahkan. +

+
+ )} + + {/* Custom Selection Info */} + {accountCategory === "custom" && ( +
+

+ {selectedAccount.length} user terpilih akan ditambahkan ke campaign ini.

)} diff --git a/components/form/blog/blog-form.tsx b/components/form/blog/blog-form.tsx index 687b2d44..25185324 100644 --- a/components/form/blog/blog-form.tsx +++ b/components/form/blog/blog-form.tsx @@ -32,6 +32,7 @@ import { postBlog, uploadThumbnailBlog } from "@/service/blog/blog"; import dynamic from "next/dynamic"; import { error } from "console"; import { loading } from "@/lib/swal"; +import { getCookiesDecrypt } from "@/lib/utils"; const taskSchema = z.object({ title: z.string().min(1, { message: "Judul diperlukan" }), @@ -92,6 +93,7 @@ export default function FormBlog() { const [thumbnail, setThumbnail] = useState(null); const [preview, setPreview] = useState(null); const inputRef = useRef(null); + const roleName = getCookiesDecrypt("urne"); const [unitSelection, setUnitSelection] = useState({ allUnit: false, @@ -180,6 +182,7 @@ export default function FormBlog() { const save = async (data: TaskSchema) => { loading(); + console.log("roleName", roleName); const finalTags = tags.join(", "); const requestData = { ...data, @@ -190,6 +193,7 @@ export default function FormBlog() { metadata: data.meta, tags: finalTags, isDraft, + isInternational: roleName?.includes("INT") ? true : false, }; const response = await postBlog(requestData); diff --git a/service/broadcast/broadcast.ts b/service/broadcast/broadcast.ts index bd876c60..29bae757 100644 --- a/service/broadcast/broadcast.ts +++ b/service/broadcast/broadcast.ts @@ -63,8 +63,8 @@ export async function saveMediaBlastCampaign(data: { return httpPostInterceptor(url, data); } -export async function saveMediaBlastCampaignAccount(data: any) { - const url = `media/blast/campaign-account`; +export async function saveMediaBlastCampaignAccountBulk(data: any) { + const url = `media/blast/campaign-account-bulk`; return httpPostInterceptor( url, data ); } @@ -111,7 +111,7 @@ export async function getMediaBlastAccount(id: string) { } export async function deleteMediaBlastAccount(id: string) { - const url = `media/blast/account?id=${id}`; + const url = `media/blast/campaign-account?id=${id}`; return httpDeleteInterceptor(url); } From 7595a5126d6cca049b47efc550a23cffecdc6b13 Mon Sep 17 00:00:00 2001 From: Sabda Yagra Date: Mon, 1 Sep 2025 22:12:21 +0700 Subject: [PATCH 04/11] fix: ckeditor and Table task-ta --- .../task-ta/components/columns.tsx | 8 +- .../task-ta/components/task-ta-table.tsx | 36 +++++--- components/editor/custom-editor.js | 88 ++++++++++++++----- components/form/task-ta/task-ta-form.tsx | 4 +- service/task.ts | 24 +++++ 5 files changed, 125 insertions(+), 35 deletions(-) diff --git a/app/[locale]/(protected)/contributor/task-ta/components/columns.tsx b/app/[locale]/(protected)/contributor/task-ta/components/columns.tsx index 6960c750..e2be3388 100644 --- a/app/[locale]/(protected)/contributor/task-ta/components/columns.tsx +++ b/app/[locale]/(protected)/contributor/task-ta/components/columns.tsx @@ -23,7 +23,7 @@ import Swal from "sweetalert2"; import { useTranslations } from "next-intl"; const useTableColumns = () => { - const t = useTranslations("Table"); // Panggil di dalam hook + const t = useTranslations("Table"); const columns: ColumnDef[] = [ { accessorKey: "no", @@ -50,6 +50,12 @@ const useTableColumns = () => { ), }, + { + accessorKey: "uniqueCode", + header: t("code", { defaultValue: "Code" }), + cell: ({ row }) => {row.getValue("uniqueCode")}, + }, + { accessorKey: "createdAt", header: t("upload-date", { defaultValue: "Upload Date" }), diff --git a/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx b/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx index 1c5e2acb..1832eefc 100644 --- a/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx +++ b/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx @@ -54,7 +54,7 @@ import { Badge } from "@/components/ui/badge"; import { useRouter, useSearchParams } from "next/navigation"; import TablePagination from "@/components/table/table-pagination"; import columns from "./columns"; -import { listTask, listTaskTa } from "@/service/task"; +import { listTask, listTaskMabesForTa, listTaskTa } from "@/service/task"; import { Label } from "@/components/ui/label"; import { format } from "date-fns"; import { useTranslations } from "next-intl"; @@ -133,15 +133,25 @@ const TaskTaTable = () => { ? format(new Date(dateFilter), "yyyy-MM-dd") : ""; try { - const res = await listTaskTa( - page - 1, - search, - showData, - filterByCode, - formattedStartDate, - isSpecificAttention ? "atensi-khusus" : "tugas-harian", - statusFilter - ); + const res = isSpecificAttention + ? await listTaskTa( + page - 1, + search, + showData, + filterByCode, + formattedStartDate, + "atensi-khusus", + statusFilter + ) + : await listTask( + page - 1, + search, + showData, + filterByCode, + formattedStartDate, + "atensi-khusus", + statusFilter + ); const data = res?.data?.data; const contentData = data?.content; @@ -210,7 +220,7 @@ const TaskTaTable = () => { } px-[18px] py-1 transition duration-100 rounded`} > - {t("special-attention", { defaultValue: "Special Attention" })} + Atensi Khusus TA{" "} { px-[18px] py-1 transition duration-100 rounded `} > - {t("daily-tasks", { defaultValue: "Daily Tasks" })} + {t("special-attention", { + defaultValue: "Special Attention", + })}{" "}
diff --git a/components/editor/custom-editor.js b/components/editor/custom-editor.js index a3f7ed77..07fd3df6 100644 --- a/components/editor/custom-editor.js +++ b/components/editor/custom-editor.js @@ -6,7 +6,7 @@ import Editor from "ckeditor5-custom-build"; function CustomEditor(props) { const maxHeight = props.maxHeight || 600; // Default max height 600px - + return (
+//
+// ); +// } + +// export default ViewEditor; diff --git a/components/form/content/spit-convert-form.tsx b/components/form/content/spit-convert-form.tsx index f796674e..3d87fdf2 100644 --- a/components/form/content/spit-convert-form.tsx +++ b/components/form/content/spit-convert-form.tsx @@ -729,7 +729,7 @@ export default function FormConvertSPIT() {
{/* Main Content */} -
+
{/* Basic Information */} @@ -797,138 +797,40 @@ export default function FormConvertSPIT() { - {/* Original Content */} + {/* Pilih Upload Type */}
- - ( - - )} - /> + + + setSelectedFileType(value) + } + className="grid grid-cols-2 gap-4" + > +
+ + +
+
+ + +
+
- {/* Content Rewrite */} -
- - -
-
- - -
- -
- - {showRewriteEditor && ( -
- {articleIds.length > 0 && ( -
- {articleIds.map((articleId, index) => ( - - ))} -
- )} - -
- - ( - - )} - /> -
-
- )} -
-
-
- - {/* - - - - Content Editor - - - - - setSelectedFileType(value) - } - className="grid grid-cols-2 gap-4" - > -
- - -
-
- - -
-
- - {/* Original Content */} - {/* {selectedFileType === "original" && ( -
- + {/* Tampilkan keduanya berdampingan */} +
+ {/* Original Content */} +
+
- )} */} - {/* Content Rewrite */} - {/* {selectedFileType === "rewrite" && ( -
+ {/* Rewrite Content */} +
+ +
@@ -953,7 +863,7 @@ export default function FormConvertSPIT() { onValueChange={setSelectedWritingStyle} > - + {WRITING_STYLES.map((style) => ( @@ -1024,359 +934,367 @@ export default function FormConvertSPIT() {
)}
- )} +
- */} + {/* Media Files */} - {detailThumb.length > 0 && ( - - - - - Media Files - - - -
- - {detailThumb.map((item) => ( - - {item.contentType === "VIDEO" ? ( -
-
-
- {/* main video player */} -
diff --git a/components/form/planning/mediahub-publish.tsx b/components/form/planning/mediahub-publish.tsx index 8d81f7bb..97c1ad56 100644 --- a/components/form/planning/mediahub-publish.tsx +++ b/components/form/planning/mediahub-publish.tsx @@ -87,13 +87,13 @@ export default function PublishMediahub() { const t = useTranslations("Form"); const [mainType, setMainType] = useState(1); const [taskType, setTaskType] = useState("atensi-khusus"); - const [broadcastType, setBroadcastType] = useState("all"); // untuk Tipe Penugasan + const [broadcastType, setBroadcastType] = useState("all"); const [type, setType] = useState("1"); const [selectedTarget, setSelectedTarget] = useState("all"); const [startDate, setStartDate] = useState(new Date()); const [detail, setDetail] = useState(); const [refresh] = useState(false); - const [listDest, setListDest] = useState([]); // Data Polda dan Polres + const [listDest, setListDest] = useState([]); const [checkedLevels, setCheckedLevels] = useState(new Set()); const [expandedPolda, setExpandedPolda] = useState([{}]); const [isLoading, setIsLoading] = useState(false); @@ -267,7 +267,7 @@ export default function PublishMediahub() { }; if (id) { - requestData.id = parseInt(id, 10); // Ensure id is a number + requestData.id = parseInt(id, 10); } console.log("Form Data Submitted:", requestData); @@ -491,7 +491,7 @@ export default function PublishMediahub() { variant="outline" size="md" className={cn( - " justify-between text-left font-normal border-default-200 text-default-600 md:px-4 w-3/12", + " justify-between text-left font-normal border-default-200 text-default-600 md:px-4 w-3/12 dark:border dark:border-gray-500", !startDate && "text-muted-foreground" )} > diff --git a/components/form/task/task-edit-form.tsx b/components/form/task/task-edit-form.tsx index 833ee76f..4d864033 100644 --- a/components/form/task/task-edit-form.tsx +++ b/components/form/task/task-edit-form.tsx @@ -994,7 +994,7 @@ export default function FormTaskEdit() {
@@ -1034,7 +1034,7 @@ export default function FormTaskEdit() {
diff --git a/components/form/task/task-form.tsx b/components/form/task/task-form.tsx index 7a360d0d..1767490b 100644 --- a/components/form/task/task-form.tsx +++ b/components/form/task/task-form.tsx @@ -42,6 +42,7 @@ import { loading } from "@/lib/swal"; import { useTranslations } from "next-intl"; import dynamic from "next/dynamic"; import UpdateSection from "@/app/[locale]/(public)/inbox/update/page"; +import { getCookiesDecrypt } from "@/lib/utils"; const taskSchema = z.object({ title: z.string().min(1, { message: "Judul diperlukan" }), @@ -98,12 +99,11 @@ export default function FormTask() { const MySwal = withReactContent(Swal); const router = useRouter(); const editor = useRef(null); - + const levelNumber = Number(getCookiesDecrypt("ulne")) || 0; type TaskSchema = z.infer; const { id } = useParams() as { id: string }; console.log(id); const [listDest, setListDest] = useState([]); - // State for various form fields const [taskOutput, setTaskOutput] = useState({ all: false, @@ -146,12 +146,14 @@ export default function FormTask() { polres: false, satker: false, }); - + // State untuk melacak apakah perubahan berasal dari checkbox Penerima Tugas - const [isUpdatingFromPenerimaTugas, setIsUpdatingFromPenerimaTugas] = useState(false); + const [isUpdatingFromPenerimaTugas, setIsUpdatingFromPenerimaTugas] = + useState(false); // State untuk melacak jenis perubahan spesifik - const [penerimaTugasChangeType, setPenerimaTugasChangeType] = useState(""); - + const [penerimaTugasChangeType, setPenerimaTugasChangeType] = + useState(""); + const [links, setLinks] = useState([""]); const { register, @@ -201,12 +203,14 @@ export default function FormTask() { setCheckedLevels((prev) => { const updatedLevels = new Set(prev); const isCurrentlyChecked = updatedLevels.has(levelId); - + if (isCurrentlyChecked) { updatedLevels.delete(levelId); - + // Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya - const poldaItem = listDest.find((item: any) => Number(item.id) === levelId); + const poldaItem = listDest.find( + (item: any) => Number(item.id) === levelId + ); if (poldaItem && poldaItem.subDestination) { poldaItem.subDestination.forEach((polres: any) => { updatedLevels.delete(Number(polres.id)); @@ -217,7 +221,7 @@ export default function FormTask() { } return updatedLevels; }); - + // Update unitSelection berdasarkan perubahan di modal updateUnitSelectionFromModal(); }; @@ -233,7 +237,7 @@ export default function FormTask() { // Set flag bahwa perubahan berasal dari checkbox Penerima Tugas setIsUpdatingFromPenerimaTugas(true); setPenerimaTugasChangeType(key + (value ? "_checked" : "_unchecked")); - + if (key === "allUnit") { const newState = { allUnit: value, @@ -247,19 +251,22 @@ export default function FormTask() { // Validasi khusus untuk POLRES if (key === "polres" && value) { // Cek apakah ada POLDA yang sudah dichecklist di modal - const hasCheckedPolda = listDest.some((item: any) => - item.levelNumber === 2 && - item.name !== "SATKER POLRI" && - checkedLevels.has(Number(item.id)) + const hasCheckedPolda = listDest.some( + (item: any) => + item.levelNumber === 2 && + item.name !== "SATKER POLRI" && + checkedLevels.has(Number(item.id)) ); - + if (!hasCheckedPolda) { // Jika tidak ada POLDA yang dichecklist di modal, tampilkan peringatan dan batalkan - alert("Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."); + alert( + "Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES." + ); return; // Batalkan perubahan } } - + const updatedSelection = { ...unitSelection, [key]: value, @@ -396,52 +403,67 @@ export default function FormTask() { const updateUnitSelectionFromModal = () => { setTimeout(() => { // Hitung total item yang tersedia untuk setiap kategori - const totalPolda = listDest.filter((item: any) => - item.levelNumber === 2 && item.name !== "SATKER POLRI" + const totalPolda = listDest.filter( + (item: any) => item.levelNumber === 2 && item.name !== "SATKER POLRI" ).length; - + const totalPolres = listDest.reduce((total: number, item: any) => { if (item.subDestination) { return total + item.subDestination.length; } return total; }, 0); - - const satkerItem = listDest.find((item: any) => item.name === "SATKER POLRI"); - const totalSatker = satkerItem ? (1 + (satkerItem.subDestination?.length || 0)) : 0; - + + const satkerItem = listDest.find( + (item: any) => item.name === "SATKER POLRI" + ); + const totalSatker = satkerItem + ? 1 + (satkerItem.subDestination?.length || 0) + : 0; + // Hitung item yang dichecklist untuk setiap kategori - const checkedPoldaCount = listDest.filter((item: any) => - item.levelNumber === 2 && - item.name !== "SATKER POLRI" && - checkedLevels.has(Number(item.id)) + const checkedPoldaCount = listDest.filter( + (item: any) => + item.levelNumber === 2 && + item.name !== "SATKER POLRI" && + checkedLevels.has(Number(item.id)) ).length; - + const checkedPolresCount = listDest.reduce((total: number, item: any) => { if (item.subDestination) { - return total + item.subDestination.filter((sub: any) => checkedLevels.has(Number(sub.id))).length; + return ( + total + + item.subDestination.filter((sub: any) => + checkedLevels.has(Number(sub.id)) + ).length + ); } return total; }, 0); - - const checkedSatkerCount = satkerItem ? ( - (checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) + - (satkerItem.subDestination?.filter((sub: any) => checkedLevels.has(Number(sub.id))).length || 0) - ) : 0; - + + const checkedSatkerCount = satkerItem + ? (checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) + + (satkerItem.subDestination?.filter((sub: any) => + checkedLevels.has(Number(sub.id)) + ).length || 0) + : 0; + // Checkbox hanya aktif jika SEMUA item dalam kategori tersebut dichecklist - const hasCheckedPolda = totalPolda > 0 && checkedPoldaCount === totalPolda; - const hasCheckedPolres = totalPolres > 0 && checkedPolresCount === totalPolres; - const hasCheckedSatker = totalSatker > 0 && checkedSatkerCount === totalSatker; - + const hasCheckedPolda = + totalPolda > 0 && checkedPoldaCount === totalPolda; + const hasCheckedPolres = + totalPolres > 0 && checkedPolresCount === totalPolres; + const hasCheckedSatker = + totalSatker > 0 && checkedSatkerCount === totalSatker; + // Update unitSelection berdasarkan checkbox yang aktif di modal - setUnitSelection(prev => ({ + setUnitSelection((prev) => ({ ...prev, polda: hasCheckedPolda, polres: hasCheckedPolres, satker: hasCheckedSatker, // allUnit hanya true jika semua kategori terpenuhi - allUnit: hasCheckedPolda && hasCheckedPolres && hasCheckedSatker + allUnit: hasCheckedPolda && hasCheckedPolres && hasCheckedSatker, })); }, 0); }; @@ -453,29 +475,41 @@ export default function FormTask() { // Khusus untuk unchecklist POLRES: hanya unchecklist polres, pertahankan polda if (penerimaTugasChangeType === "polres_unchecked") { const newCheckedLevels = new Set(checkedLevels); - + // Hapus semua polres dari modal, tapi pertahankan polda listDest.forEach((item: any) => { - if (item.subDestination && item.levelNumber === 2 && item.name !== "SATKER POLRI") { + if ( + item.subDestination && + item.levelNumber === 2 && + item.name !== "SATKER POLRI" + ) { item.subDestination.forEach((polres: any) => { newCheckedLevels.delete(Number(polres.id)); }); } }); - + setCheckedLevels(newCheckedLevels); } // Untuk perubahan lainnya, jalankan logika normal - else if (unitSelection.polda || unitSelection.polres || unitSelection.satker) { + else if ( + unitSelection.polda || + unitSelection.polres || + unitSelection.satker + ) { // Mulai dengan checkbox yang sudah ada untuk mempertahankan pilihan manual user const newCheckedLevels = new Set(checkedLevels); - + listDest.forEach((item: any) => { // Jika polda dichecklist, checklist semua polda (levelNumber 2, bukan SATKER POLRI) - if (unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { + if ( + unitSelection.polda && + item.levelNumber === 2 && + item.name !== "SATKER POLRI" + ) { newCheckedLevels.add(Number(item.id)); } - + // Jika satker dichecklist, checklist SATKER POLRI dan sub-itemnya if (unitSelection.satker && item.name === "SATKER POLRI") { newCheckedLevels.add(Number(item.id)); @@ -485,17 +519,25 @@ export default function FormTask() { }); } } - + // Jika polres dichecklist if (unitSelection.polres && item.subDestination) { // Jika checkbox POLDA di Penerima Tugas juga aktif, checklist semua polres - if (unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { + if ( + unitSelection.polda && + item.levelNumber === 2 && + item.name !== "SATKER POLRI" + ) { item.subDestination.forEach((polres: any) => { newCheckedLevels.add(Number(polres.id)); }); } // Jika checkbox POLDA di Penerima Tugas tidak aktif, tapi ada POLDA yang dichecklist di modal - else if (!unitSelection.polda && item.levelNumber === 2 && item.name !== "SATKER POLRI") { + else if ( + !unitSelection.polda && + item.levelNumber === 2 && + item.name !== "SATKER POLRI" + ) { // Cek apakah POLDA ini sudah dichecklist di modal if (checkedLevels.has(Number(item.id))) { // Jika ya, checklist semua polres dari POLDA ini @@ -506,13 +548,13 @@ export default function FormTask() { } } }); - + setCheckedLevels(newCheckedLevels); } else { // Jika tidak ada unitSelection yang aktif, unchecklist semua item di modal setCheckedLevels(new Set()); } - + // Reset flag setelah sinkronisasi selesai setTimeout(() => { setIsUpdatingFromPenerimaTugas(false); @@ -756,7 +798,7 @@ export default function FormTask() {
- {Object.keys(unitSelection).map((key) => { + {/* {Object.keys(unitSelection).map((key) => { return (
); - })} + })} */} + {Object.keys(unitSelection) + .filter((key) => { + if (levelNumber === 2) { + return key === "polda" || key === "polres"; + } + return true; + }) + .map((key) => { + return ( +
+ + handleUnitChange( + key as keyof typeof unitSelection, + value as boolean + ) + } + /> + +
+ ); + })}
@@ -790,77 +862,83 @@ export default function FormTask() { Daftar Wilayah Polda dan Polres
- {listDest.map((polda: any) => ( -
- - {expandedPolda[polda.id] && ( -
-
+ )} +
+ ))}
From a11257ef4e9cfad8988402477c982ae61376fc71 Mon Sep 17 00:00:00 2001 From: Sabda Yagra Date: Wed, 3 Sep 2025 22:11:27 +0700 Subject: [PATCH 07/11] fixing --- .../task-ta/components/task-ta-table.tsx | 24 +++++++++---------- components/form/task/task-detail-form.tsx | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx b/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx index 038b9ce2..e6ebbf69 100644 --- a/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx +++ b/app/[locale]/(protected)/contributor/task-ta/components/task-ta-table.tsx @@ -272,6 +272,17 @@ const TaskTaTable = () => { hidden />
+ - -
{/* )} @@ -1165,7 +1165,7 @@ export default function FormTaskDetail() { {imageUploadedFiles?.length > 0 && ( )} -
+
{selectedImage && ( Date: Thu, 4 Sep 2025 00:05:14 +0700 Subject: [PATCH 08/11] fixing --- .../form/communication/collaboration-form.tsx | 152 +++++++++++++++++- .../communication/escalation-detail-form.tsx | 27 ++-- .../form/communication/internal-form.tsx | 140 +++++++++++++++- components/form/content/audio-detail-form.tsx | 33 ++-- components/form/content/image-detail-form.tsx | 53 +++--- components/form/content/teks-detail-form.tsx | 35 ++-- components/form/content/video-detail-form.tsx | 31 ++-- 7 files changed, 392 insertions(+), 79 deletions(-) diff --git a/components/form/communication/collaboration-form.tsx b/components/form/communication/collaboration-form.tsx index 530d4831..12b35c4a 100644 --- a/components/form/communication/collaboration-form.tsx +++ b/components/form/communication/collaboration-form.tsx @@ -163,7 +163,7 @@ export default function FormCollaboration() { } const save = async (data: TaskSchema) => { - setIsSubmitting(true); + setIsSubmitting(true); MySwal.fire({ title: "Menyimpan...", @@ -192,7 +192,7 @@ export default function FormCollaboration() { console.log("Form Data Submitted:", requestData); console.log("response", response); - Swal.close(); + Swal.close(); MySwal.fire({ title: "Sukses", @@ -255,6 +255,78 @@ export default function FormCollaboration() { Priority* @@ -268,7 +340,7 @@ export default function FormCollaboration() { }), }} /> -
+
*/}
@@ -283,8 +355,82 @@ export default function FormCollaboration() { onChange={handleChange} formatOptionLabel={formatOptionLabel} isMulti={false} + styles={{ + control: (base, state) => ({ + ...base, + backgroundColor: + document.documentElement.classList.contains("dark") + ? "#1f2937" // bg-gray-800 + : "#ffffff", // bg-white + color: document.documentElement.classList.contains("dark") + ? "#f9fafb" // text-gray-100 + : "#111827", // text-gray-900 + borderColor: state.isFocused + ? "#2563eb" + : base.borderColor, + boxShadow: state.isFocused + ? "0 0 0 1px #2563eb" + : base.boxShadow, + "&:hover": { + borderColor: "#2563eb", + }, + }), + menu: (base) => ({ + ...base, + backgroundColor: + document.documentElement.classList.contains("dark") + ? "#1f2937" + : "#ffffff", + color: document.documentElement.classList.contains("dark") + ? "#f9fafb" + : "#111827", + }), + option: (base, state) => ({ + ...base, + backgroundColor: state.isSelected + ? "#2563eb" + : state.isFocused + ? "#2563eb33" + : document.documentElement.classList.contains("dark") + ? "#1f2937" + : "#ffffff", + color: state.isSelected + ? "#ffffff" + : document.documentElement.classList.contains("dark") + ? "#f9fafb" + : "#111827", + cursor: "pointer", + }), + singleValue: (base) => ({ + ...base, + color: document.documentElement.classList.contains("dark") + ? "#f9fafb" + : "#111827", + }), + placeholder: (base) => ({ + ...base, + color: document.documentElement.classList.contains("dark") + ? "#9ca3af" + : "#6b7280", + }), + }} />
+ + {/*
+ +