From 1fc21c8d2bcbacd2c4316b88846081495644a544 Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Sun, 8 Jun 2025 19:06:17 +0800 Subject: [PATCH] fix ads flow --- .../admin/settings/iklan/component/column.tsx | 79 ++- .../admin/settings/iklan/detail/[id]/page.tsx | 15 + .../admin/settings/iklan/update/[id]/page.tsx | 15 + .../form/setting/form-add-iklan-detail.tsx | 635 +++++++++++++++--- .../form/setting/form-add-iklan-update.tsx | 629 ++++++++++++++--- service/settings/settings.ts | 5 + 6 files changed, 1192 insertions(+), 186 deletions(-) create mode 100644 app/[locale]/(protected)/admin/settings/iklan/detail/[id]/page.tsx create mode 100644 app/[locale]/(protected)/admin/settings/iklan/update/[id]/page.tsx diff --git a/app/[locale]/(protected)/admin/settings/iklan/component/column.tsx b/app/[locale]/(protected)/admin/settings/iklan/component/column.tsx index c137ec06..421168c9 100644 --- a/app/[locale]/(protected)/admin/settings/iklan/component/column.tsx +++ b/app/[locale]/(protected)/admin/settings/iklan/component/column.tsx @@ -34,6 +34,9 @@ import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"; import { setBanner } from "@/service/settings/settings"; import { error } from "@/config/swal"; import { useToast } from "@/components/ui/use-toast"; +import withReactContent from "sweetalert2-react-content"; +import Swal from "sweetalert2"; +import { deleteMedia } from "@/service/content/content"; const columns: ColumnDef[] = [ { @@ -74,12 +77,49 @@ const columns: ColumnDef[] = [ header: "Actions", enableHiding: false, cell: ({ row }) => { - const { toast } = useToast(); + const MySwal = withReactContent(Swal); - const handleBanner = async (id: number) => { - const response = setBanner(id, true); - toast({ - title: "Success", + async function doDelete(id: any) { + // loading(); + const data = { + id, + }; + + const response = await deleteMedia(data); + + if (response?.error) { + error(response.message); + return false; + } + success(); + } + + function success() { + MySwal.fire({ + title: "Sukses", + icon: "success", + confirmButtonColor: "#3085d6", + confirmButtonText: "OK", + }).then((result) => { + if (result.isConfirmed) { + window.location.reload(); + } + }); + } + + const handleDeleteMedia = (id: any) => { + MySwal.fire({ + title: "Hapus Data", + text: "", + icon: "warning", + showCancelButton: true, + cancelButtonColor: "#3085d6", + confirmButtonColor: "#d33", + confirmButtonText: "Hapus", + }).then((result) => { + if (result.isConfirmed) { + doDelete(id); + } }); }; return ( @@ -93,17 +133,24 @@ const columns: ColumnDef[] = [ - - - Detail - - - - handleBanner(row.original.id)}> - Jadikan Banner - + + + + View + + + + + + Edit + + + handleDeleteMedia(row.original.id)} + className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none" + > + + Delete diff --git a/app/[locale]/(protected)/admin/settings/iklan/detail/[id]/page.tsx b/app/[locale]/(protected)/admin/settings/iklan/detail/[id]/page.tsx new file mode 100644 index 00000000..7d3e71b8 --- /dev/null +++ b/app/[locale]/(protected)/admin/settings/iklan/detail/[id]/page.tsx @@ -0,0 +1,15 @@ +import SiteBreadcrumb from "@/components/site-breadcrumb"; +import { TambahIklanDetail } from "@/components/form/setting/form-add-iklan-detail"; + +const AdvertisementsDetailPage = () => { + return ( +
+ +
+ +
+
+ ); +}; + +export default AdvertisementsDetailPage; diff --git a/app/[locale]/(protected)/admin/settings/iklan/update/[id]/page.tsx b/app/[locale]/(protected)/admin/settings/iklan/update/[id]/page.tsx new file mode 100644 index 00000000..3039236f --- /dev/null +++ b/app/[locale]/(protected)/admin/settings/iklan/update/[id]/page.tsx @@ -0,0 +1,15 @@ +import SiteBreadcrumb from "@/components/site-breadcrumb"; +import { TambahIklanUpdate } from "@/components/form/setting/form-add-iklan-update"; + +const AdvertisementsUpdatePage = () => { + return ( +
+ +
+ +
+
+ ); +}; + +export default AdvertisementsUpdatePage; diff --git a/components/form/setting/form-add-iklan-detail.tsx b/components/form/setting/form-add-iklan-detail.tsx index 8430568a..e6b8e542 100644 --- a/components/form/setting/form-add-iklan-detail.tsx +++ b/components/form/setting/form-add-iklan-detail.tsx @@ -13,96 +13,561 @@ import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Plus } from "lucide-react"; +import { ChevronDown, ChevronUp, Plus } from "lucide-react"; +import { Card } from "@/components/ui/card"; +import Image from "next/image"; +import { Upload } from "tus-js-client"; +import { getCsrfToken } from "@/service/auth"; +import { error, loading } from "@/lib/swal"; +import { format, parseISO } from "date-fns"; +import { getUserLevelForAssignments } from "@/service/task"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Controller, useForm } from "react-hook-form"; +import withReactContent from "sweetalert2-react-content"; +import { useTranslations } from "next-intl"; +import Swal from "sweetalert2"; +import { z } from "zod"; +import { DateRange } from "react-day-picker"; +import { postCalendar } from "@/service/schedule/schedule"; +import { id } from "date-fns/locale"; +import router from "next/router"; +import { + detailAdvertisements, + postAdvertisements, +} from "@/service/settings/settings"; +import Cookies from "js-cookie"; +import { Label } from "@/components/ui/label"; +import FileUploader from "../shared/file-uploader"; +import { Icon } from "@/components/ui/icon"; +import { useParams } from "next/navigation"; +import Link from "next/link"; -export function TambahIklanModalDetail() { +const calendarSchema = z.object({ + title: z.string().min(1, { message: "Judul diperlukan" }), + description: z.string().min(1, { message: "Judul diperlukan" }), +}); + +interface FileWithPreview extends File { + preview: string; +} + +interface FileUploaded { + id: number; + url: string; +} + +interface Detail { + id: number; + title: string; + description: string; +} + +export function TambahIklanDetail() { const [open, setOpen] = React.useState(false); + const MySwal = withReactContent(Swal); + const t = useTranslations("Schedule"); + const { id } = useParams() as { id: string }; + type CalendarSchema = z.infer; + const [eventDate, setEventDate] = React.useState(new Date()); + const [listDest, setListDest] = React.useState([]); + const [checkedLevels, setCheckedLevels] = React.useState(new Set()); + const [expandedPolda, setExpandedPolda] = React.useState([{}]); + const [isLoading, setIsLoading] = React.useState(false); + const [isImageUploadFinish, setIsImageUploadFinish] = React.useState(false); + const [files, setFiles] = React.useState([]); + const [selectedPlacement, setSelectedPlacement] = React.useState(""); + const [imageUploadedFiles, setImageUploadedFiles] = React.useState< + FileUploaded[] + >([]); + const [detail, setDetail] = React.useState(); + const [refresh, setRefresh] = React.useState(false); + const [imageFiles, setImageFiles] = React.useState([]); + const [date, setDate] = React.useState({ + from: new Date(2025, 0, 1), + }); + const [unitSelection, setUnitSelection] = React.useState({ + semua: false, + mabes: false, + polda: false, + satker: false, + internasional: false, + }); + + const { + control, + handleSubmit, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(calendarSchema), + defaultValues: { + description: "", + }, + }); + + const handlePlacementSelect = (value: string) => { + setSelectedPlacement(value); + }; + + React.useEffect(() => { + async function initState() { + if (id) { + const response = await detailAdvertisements(id); + const details = response?.data?.data; + + setDetail(details); + if (details?.assignedToLevel) { + const levelIds = details.assignedToLevel + .split(",") + .map((id: string) => parseInt(id)); + setCheckedLevels(new Set(levelIds)); + } + + if (details?.placements) { + setSelectedPlacement(details.placements); // "left-bottom", etc. + } + } + } + initState(); + }, [refresh, setValue]); + + React.useEffect(() => { + async function fetchPoldaPolres() { + setIsLoading(true); + try { + const response = await getUserLevelForAssignments(); + setListDest(response?.data?.data.list); + console.log("polda", response?.data?.data?.list); + const initialExpandedState = response?.data?.data.list.reduce( + (acc: any, polda: any) => { + acc[polda.id] = false; + return acc; + }, + {} + ); + setExpandedPolda(initialExpandedState); + console.log("polres", initialExpandedState); + } catch (error) { + console.error("Error fetching Polda/Polres data:", error); + } finally { + setIsLoading(false); + } + } + fetchPoldaPolres(); + }, []); + + const handleCheckboxChange = (levelId: number) => { + setCheckedLevels((prev) => { + const updatedLevels = new Set(prev); + if (updatedLevels.has(levelId)) { + updatedLevels.delete(levelId); + } else { + updatedLevels.add(levelId); + } + return updatedLevels; + }); + }; + + const handlePoldaPolresChange = () => { + return Array.from(checkedLevels).join(","); + }; + + const handleUnitChange = ( + key: keyof typeof unitSelection, + value: boolean + ) => { + if (key === "semua") { + const newState = { + semua: value, + mabes: value, + polda: value, + satker: value, + internasional: value, + }; + setUnitSelection(newState); + } else { + const updatedSelection = { + ...unitSelection, + [key]: value, + }; + + const allChecked = ["mabes", "polda", "satker", "internasional"].every( + (k) => updatedSelection[k as keyof typeof unitSelection] + ); + + updatedSelection.semua = allChecked; + + setUnitSelection(updatedSelection); + } + }; + + const toggleExpand = (poldaId: any) => { + setExpandedPolda((prev: any) => ({ + ...prev, + [poldaId]: !prev[poldaId], + })); + }; + + const save = async (data: CalendarSchema) => { + const unitMapping = { + allUnit: "0", + mabes: "1", + polda: "2", + satker: "4", + internasional: "5", + }; + const assignmentToString = Object.keys(unitSelection) + .filter((key) => unitSelection[key as keyof typeof unitSelection]) + .map((key) => unitMapping[key as keyof typeof unitMapping]) + .join(","); + + const formMedia = new FormData(); + formMedia.append("title", data.title); + formMedia.append("placements", selectedPlacement); + formMedia.append("description", data.description); + formMedia.append("redirectLink", "https://new.netidhub.com"); + formMedia.append("assignedToLevel", handlePoldaPolresChange()); + formMedia.append("file", imageFiles[0]); + + console.log("Form Data Submitted:", formMedia); + + loading(); + const response = await postAdvertisements(formMedia); + if (response?.error) { + error(response?.message); + return false; + } + close(); + + Cookies.set("scheduleId", response?.data?.data.id, { + expires: 1, + }); + }; + + async function uploadResumableFile( + idx: number, + id: string, + file: any, + fileTypeId: string, + duration: string + ) { + console.log(idx, id, file, fileTypeId, duration); + + const resCsrf = await getCsrfToken(); + const csrfToken = resCsrf?.data?.token; + console.log("CSRF TOKEN : ", csrfToken); + const headers = { + "X-XSRF-TOKEN": csrfToken, + }; + + const upload = new Upload(file, { + endpoint: `${process.env.NEXT_PUBLIC_API}/advertisements/file/upload`, + headers: headers, + retryDelays: [0, 3000, 6000, 12_000, 24_000], + chunkSize: 20_000, + metadata: { + advertisementsId: id, + filename: file.name, + contentType: file.type, + fileTypeId: fileTypeId, + duration, + }, + onBeforeRequest: function (req) { + var xhr = req.getUnderlyingObject(); + xhr.withCredentials = true; + }, + onError: async (e: any) => { + console.log("Error upload :", e); + error(e); + }, + onChunkComplete: ( + chunkSize: any, + bytesAccepted: any, + bytesTotal: any + ) => { + // const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100); + // progressInfo[idx].percentage = uploadPersen; + // counterUpdateProgress++; + // console.log(counterUpdateProgress); + // setProgressList(progressInfo); + // setCounterProgress(counterUpdateProgress); + }, + onSuccess: async () => { + // uploadPersen = 100; + // progressInfo[idx].percentage = 100; + // counterUpdateProgress++; + // setCounterProgress(counterUpdateProgress); + successTodo(); + if (fileTypeId == "1") { + setIsImageUploadFinish(true); + } + }, + }); + + upload.start(); + } + + React.useEffect(() => { + successTodo(); + }, [isImageUploadFinish]); + + function successTodo() { + if (isImageUploadFinish) { + successSubmit("/in/admin/settings/iklan"); + } + } + + const successSubmit = (redirect: string) => { + MySwal.fire({ + title: "Sukses", + text: "Data berhasil disimpan.", + icon: "success", + confirmButtonColor: "#3085d6", + confirmButtonText: "OK", + }).then(() => { + router.push(redirect); + }); + }; + + const onSubmit = (data: CalendarSchema) => { + MySwal.fire({ + title: "Simpan Data", + text: "Apakah Anda yakin ingin menyimpan data ini?", + icon: "warning", + showCancelButton: true, + cancelButtonColor: "#d33", + confirmButtonColor: "#3085d6", + confirmButtonText: "Simpan", + }).then((result) => { + if (result.isConfirmed) { + save(data); + } + }); + }; + + const renderFilePreview = (url: string) => { + return ( + {"file + ); + }; + + const handleRemoveFile = (id: number) => {}; return ( - - - - - - - Tambah Iklan - +
+ + {detail !== undefined ? ( +
+
+
+

Target Area

+
+ {[ + { label: "Kiri - 1", value: "left-top" }, + { label: "Kiri - 2", value: "left-bottom" }, + { label: "Kanan - 1", value: "right-top" }, + { label: "Kanan - 2", value: "right-bottom" }, + ].map(({ label, value }) => ( + + ))} +
+
-
-
-

Target Area

-
- {["Kiri - 1", "Kiri - 2", "Kanan - 1", "Kanan - 2"].map( - (label) => ( - - ) - )} + {/*
+

Publish Area

+
+
+ {Object.keys(unitSelection).map((key) => ( +
+ + handleUnitChange( + key as keyof typeof unitSelection, + value as boolean + ) + } + /> + +
+ ))} +
+
+ + + + + + + + Daftar Wilayah Polda dan Polres + + +
+ {listDest.map((polda: any) => ( +
+ + {expandedPolda[polda.id] && ( +
+ + {polda?.subDestination?.map((polres: any) => ( + + ))} +
+ )} +
+ ))} +
+
+
+
+
+
*/} + +
+

Nama Iklan

+ ( + + )} + /> + {errors.title?.message && ( +

{errors.title.message}

+ )} +
+ +
+ +

+ (Warning: Foto yang di upload adalah Foto Potrait) +

+ + Thumbnail Gambar Utama + +
+
+

Deskripsi

+ ( +