This commit is contained in:
hanif salafi 2025-07-28 14:10:01 +07:00
commit 2fae4027ee
10 changed files with 548 additions and 345 deletions

View File

@ -1,4 +1,4 @@
'use client' "use client";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
@ -35,7 +35,7 @@ import { Button } from "@/components/ui/button";
import TablePagination from "@/components/table/table-pagination"; import TablePagination from "@/components/table/table-pagination";
import { import {
getMediaBlastCampaignById, getMediaBlastCampaignById,
getMediaBlastBroadcastList getMediaBlastBroadcastList,
} from "@/service/broadcast/broadcast"; } from "@/service/broadcast/broadcast";
// Types // Types
@ -69,19 +69,27 @@ interface PageProps {
}; };
} }
export default function BroadcastCampaignDetail({ params, searchParams }: PageProps) { export default function BroadcastCampaignDetail({
params,
searchParams,
}: PageProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { id, locale } = params; const { id, locale } = params;
const [getData, setGetData] = useState<CampaignData[]>([]); const [getData, setGetData] = useState<CampaignData[]>([]);
const [totalPage, setTotalPage] = useState<number>(0); const [totalPage, setTotalPage] = useState<number>(0);
const [totalData, setTotalData] = useState<number>(0); const [totalData, setTotalData] = useState<number>(0);
const [activeTab, setActiveTab] = useState<"sent" | "schedule" | "account-list">("sent"); const [activeTab, setActiveTab] = useState<
"sent" | "schedule" | "account-list"
>("sent");
const { page, size } = searchParams; const { page, size } = searchParams;
const [calenderState, setCalenderState] = useState<boolean>(false); const [calenderState, setCalenderState] = useState<boolean>(false);
const [typeFilter, setTypeFilter] = useState<string>("email"); const [typeFilter, setTypeFilter] = useState<string>("email");
const [dateRange, setDateRange] = useState<[Date, Date]>([new Date(), new Date()]); const [dateRange, setDateRange] = useState<[Date, Date]>([
new Date(),
new Date(),
]);
const [startDate, endDate] = dateRange; const [startDate, endDate] = dateRange;
const [startDateString, setStartDateString] = useState<string | undefined>(); const [startDateString, setStartDateString] = useState<string | undefined>();
@ -107,7 +115,7 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
const setCurrentPage = (pageNumber: number) => { const setCurrentPage = (pageNumber: number) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString()); params.set("page", pageNumber.toString());
router.push(`${pathname}?${params.toString()}`); router.push(`${pathname}?${params.toString()}`);
}; };
@ -138,7 +146,14 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
useEffect(() => { useEffect(() => {
getListPaginationData(); getListPaginationData();
}, [currentPage, pageSize, activeTab, endDateString, startDateString, typeFilter]); }, [
currentPage,
pageSize,
activeTab,
endDateString,
startDateString,
typeFilter,
]);
function setupData(rawData: PaginatedResponse) { function setupData(rawData: PaginatedResponse) {
console.log("raw", rawData); console.log("raw", rawData);
@ -167,8 +182,13 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
accessorKey: "mediaBlastCampaign.title", accessorKey: "mediaBlastCampaign.title",
header: "Campaign", header: "Campaign",
cell: ({ row }) => ( cell: ({ row }) => (
<Link href={`/${locale}/admin/broadcast/campaign-list/detail/${row.original.mediaBlastCampaignId}`} className="text-dark"> <Link
<span className="font-weight-bold">{row.original.mediaBlastCampaign?.title}</span> href={`/${locale}/admin/broadcast/campaign-list/detail/${row.original.mediaBlastCampaignId}`}
className="text-dark"
>
<span className="font-weight-bold">
{row.original.mediaBlastCampaign?.title}
</span>
</Link> </Link>
), ),
}, },
@ -176,7 +196,10 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
accessorKey: "subject", accessorKey: "subject",
header: "Judul", header: "Judul",
cell: ({ row }) => ( cell: ({ row }) => (
<Link href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`} className="text-dark"> <Link
href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`}
className="text-dark"
>
<span className="font-weight-bold">{row.getValue("subject")}</span> <span className="font-weight-bold">{row.getValue("subject")}</span>
</Link> </Link>
), ),
@ -185,29 +208,23 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
accessorKey: "type", accessorKey: "type",
header: "Tipe", header: "Tipe",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right text-black"> <div className="text-right text-black">{row.getValue("type")}</div>
{row.getValue("type")} ),
</div>
)
}, },
{ {
accessorKey: "status", accessorKey: "status",
header: "Status", header: "Status",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right text-black"> <div className="text-right text-black">{row.getValue("status")}</div>
{row.getValue("status")} ),
</div>
)
}, },
{ {
accessorKey: "sendDate", accessorKey: "sendDate",
header: "Tanggal & Waktu", header: "Tanggal & Waktu",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-black"> <div className="text-black">{row.getValue("sendDate")}</div>
{row.getValue("sendDate")} ),
</div> },
)
}
]; ];
const table = useReactTable({ const table = useReactTable({
@ -238,7 +255,7 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
setEndDateString(getOnlyDate(endDate)); setEndDateString(getOnlyDate(endDate));
} }
} }
console.log('date range', dateRange); console.log("date range", dateRange);
initState(); initState();
}, [calenderState, startDate, endDate]); }, [calenderState, startDate, endDate]);
@ -247,7 +264,7 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
}; };
return ( return (
<div className="bg-white container-fluid rounded rounded-xl"> <div className="bg-white container-fluid rounded ">
<div className="mt-1 p-4"> <div className="mt-1 p-4">
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-4"> <div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-4">
<Button <Button
@ -298,7 +315,7 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
? "bg-black text-white " ? "bg-black text-white "
: "bg-white text-black " : "bg-white text-black "
}`} }`}
size='sm' size="sm"
> >
Email Blast Email Blast
</Button> </Button>
@ -309,7 +326,7 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
? "bg-black text-white " ? "bg-black text-white "
: "bg-white text-black " : "bg-white text-black "
}`} }`}
size='sm' size="sm"
> >
WhatsApp Blast WhatsApp Blast
</Button> </Button>
@ -359,14 +376,20 @@ export default function BroadcastCampaignDetail({ params, searchParams }: PagePr
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results. No results.
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -0,0 +1,11 @@
import DetailContentBlast from "@/components/form/broadcast/content-blast--detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
export default function DetailEmailBlast() {
return (
<div>
<SiteBreadcrumb />
<DetailContentBlast />
</div>
);
}

View File

@ -0,0 +1,96 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { getMediaBlastBroadCast } from "@/service/broadcast/broadcast";
import { loading, close } from "@/config/swal";
interface BroadcastDetail {
id: number;
body: string;
subject: string;
sendTime: string;
thumbnail: string;
contentUrl: string;
}
export default function DetailContentBlast() {
const params = useParams();
const { id } = params as { id: string };
const [detail, setDetail] = useState<BroadcastDetail | null>(null);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
fetchDetailData();
}, [id]);
async function fetchDetailData() {
loading();
try {
const res = await getMediaBlastBroadCast(id);
close();
const detailData = res?.data?.data;
if (detailData && detailData.id === Number(id)) {
setDetail({
id: detailData.id,
body: detailData.body,
subject: detailData.subject,
sendTime: detailData.sendTime,
thumbnail: detailData.thumbnail,
contentUrl: detailData.contentUrl,
});
} else {
setNotFound(true);
}
} catch (error) {
close();
console.error("Failed to fetch broadcast detail:", error);
setNotFound(true);
}
}
if (notFound) {
return <div className="text-red-500 p-4">Data tidak ditemukan.</div>;
}
if (!detail) {
return <div className="text-gray-500 p-4">Loading preview...</div>;
}
return (
<div className="bg-white rounded-md w-full p-4">
<p className="text-xl font-semibold py-1">Preview</p>
<div className="bg-[#ddf7c8] p-4 rounded-xl mx-auto space-y-4">
<div className="flex gap-4">
<Image
src={detail.thumbnail}
alt="Media Thumbnail"
width={250}
height={200}
className="rounded-md object-cover"
/>
</div>
<div className="text-sm text-black font-bold">{detail.subject}</div>
<p>
Selengkapnya silakan cek di sini:{" "}
<a
href={detail.contentUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline"
>
{detail.contentUrl}
</a>
</p>
<div className="text-right text-xs text-gray-500">
{detail.sendTime}
</div>
</div>
</div>
);
}

View File

@ -69,6 +69,7 @@ import { getCookiesDecrypt } from "@/lib/utils";
import { error } from "@/lib/swal"; import { error } from "@/lib/swal";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import { close, loading } from "@/config/swal";
// Types // Types
interface Category { interface Category {
@ -163,6 +164,7 @@ export default function FormConvertSPIT() {
const router = useRouter(); const router = useRouter();
const t = useTranslations("Form"); const t = useTranslations("Form");
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
const [isAlreadySaved, setIsAlreadySaved] = useState(false);
// Form state // Form state
const { const {
@ -198,35 +200,33 @@ export default function FormConvertSPIT() {
const [showRewriteEditor, setShowRewriteEditor] = useState(false); const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false); const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false);
const [isLoadingRewrite, setIsLoadingRewrite] = useState(false); const [isLoadingRewrite, setIsLoadingRewrite] = useState(false);
// Media state
const [detailThumb, setDetailThumb] = useState<string[]>([]); const [detailThumb, setDetailThumb] = useState<string[]>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null); const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [files, setFiles] = useState<FileType[]>([]); const [files, setFiles] = useState<FileType[]>([]);
const [filePlacements, setFilePlacements] = useState<string[][]>([]); const [filePlacements, setFilePlacements] = useState<string[][]>([]);
// Content rewrite state
const [articleIds, setArticleIds] = useState<string[]>([]); const [articleIds, setArticleIds] = useState<string[]>([]);
const [selectedArticleId, setSelectedArticleId] = useState<string | null>( const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null null
); );
const [articleBody, setArticleBody] = useState<string>(""); const [articleBody, setArticleBody] = useState<string>("");
// Form data state
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]); const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [inputRef] = useState(useRef<HTMLInputElement>(null)); const [inputRef] = useState(useRef<HTMLInputElement>(null));
// User permissions
const userLevelId = getCookiesDecrypt("ulie"); const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie"); const roleId = getCookiesDecrypt("urie");
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false); const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
// Initialize component
useEffect(() => { useEffect(() => {
initializeComponent(); initializeComponent();
}, []); }, []);
// useEffect(() => {
// const savedFlag = localStorage.getItem(`spit_saved_${id}`);
// if (savedFlag === "true") {
// setIsAlreadySaved(true);
// }
// }, [id]);
useEffect(() => { useEffect(() => {
checkUserPermissions(); checkUserPermissions();
}, [userLevelId, roleId]); }, [userLevelId, roleId]);
@ -297,9 +297,12 @@ export default function FormConvertSPIT() {
}; };
const loadDetail = async () => { const loadDetail = async () => {
loading()
try { try {
const response = await detailSPIT(id); const response = await detailSPIT(id);
const details = response?.data?.data; const details = response?.data?.data;
setIsAlreadySaved(details?.isPublish ? true : false)
if (!details) { if (!details) {
throw new Error("Detail not found"); throw new Error("Detail not found");
@ -341,9 +344,9 @@ export default function FormConvertSPIT() {
console.error("Failed to load detail:", error); console.error("Failed to load detail:", error);
throw error; throw error;
} }
close()
}; };
// Event handlers
const handleCategoryChange = async (categoryId: string) => { const handleCategoryChange = async (categoryId: string) => {
const id = Number(categoryId); const id = Number(categoryId);
setSelectedCategoryId(id); setSelectedCategoryId(id);
@ -602,6 +605,9 @@ export default function FormConvertSPIT() {
await convertSPIT(requestData); await convertSPIT(requestData);
// localStorage.setItem(`spit_saved_${id}`, "true");
// setIsAlreadySaved(true);
MySwal.fire({ MySwal.fire({
title: "Success", title: "Success",
text: "Data saved successfully", text: "Data saved successfully",
@ -609,7 +615,8 @@ export default function FormConvertSPIT() {
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
}).then(() => { }).then(() => {
// router.push("/in/contributor/content/spit"); // router.push("/in/contributor/content/spit");
router.replace(`${window.location.pathname}?id=${id}`); // router.replace(`${window.location.pathname}?id=${id}`);
loadDetail()
}); });
} catch (error) { } catch (error) {
console.error("Failed to save:", error); console.error("Failed to save:", error);
@ -1164,16 +1171,29 @@ export default function FormConvertSPIT() {
<CardContent className="pt-6"> <CardContent className="pt-6">
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full mb-4"
disabled={isSubmitting || isSaving} disabled={isSubmitting || isSaving || isAlreadySaved}
> >
{isSubmitting || isSaving ? ( {isSubmitting || isSaving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : ( ) : (
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
)} )}
{isSubmitting || isSaving ? "Saving..." : "Save Changes"} {isAlreadySaved
? "Already Saved"
: isSubmitting || isSaving
? "Saving..."
: "Save Changes"}
</Button> </Button>
{isAlreadySaved && (
<Alert variant="soft">
<CheckCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-500">
Konten sudah disimpan. Anda tidak dapat menyimpan ulang.
</AlertDescription>
</Alert>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -218,8 +218,23 @@ const HeroModal = ({
}; };
}, []); }, []);
// const initFetch = async () => {
// const response = await listPopUp(
// group === "mabes"
// ? ""
// : group === "polda" && poldaName
// ? poldaName
// : group === "satker" && satkerName
// ? "satker-" + satkerName
// : "",
// locale == "en"
// );
// const interstitial = response?.data?.data || [];
// setHeroData(interstitial);
// };
const initFetch = async () => { const initFetch = async () => {
const response = await listPopUp( const response = await listStaticBanner(
group === "mabes" group === "mabes"
? "" ? ""
: group === "polda" && poldaName : group === "polda" && poldaName
@ -227,10 +242,17 @@ const HeroModal = ({
: group === "satker" && satkerName : group === "satker" && satkerName
? "satker-" + satkerName ? "satker-" + satkerName
: "", : "",
locale == "en" locale === "en"
); );
const interstitial = response?.data?.data || [];
setHeroData(interstitial); const banners = response?.data?.data || [];
const enrichedData = banners.map((item: any) => ({
...item,
fileTypeId: item?.fileType?.id ?? null,
}));
setHeroData(enrichedData);
}; };
const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => { const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {

View File

@ -2,7 +2,11 @@ import { formatDateToIndonesian, shimmer, toBase64 } from "@/utils/globals";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import "swiper/css/bundle"; import "swiper/css/bundle";
import "swiper/css/navigation"; import "swiper/css/navigation";
import { getHeroData, listPopUp, listStaticBanner } from "@/service/landing/landing"; import {
getHeroData,
listPopUp,
listStaticBanner,
} from "@/service/landing/landing";
import Link from "next/link"; import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation"; import { useParams, usePathname, useRouter } from "next/navigation";
import { import {
@ -207,8 +211,23 @@ const HeroModal = ({
}; };
}, []); }, []);
// const initFetch = async () => {
// const response = await listPopUp(
// group === "mabes"
// ? ""
// : group === "polda" && poldaName
// ? poldaName
// : group === "satker" && satkerName
// ? "satker-" + satkerName
// : "",
// locale == "en"
// );
// const interstitial = response?.data?.data || [];
// setHeroData(interstitial);
// };
const initFetch = async () => { const initFetch = async () => {
const response = await listPopUp( const response = await listStaticBanner(
group === "mabes" group === "mabes"
? "" ? ""
: group === "polda" && poldaName : group === "polda" && poldaName
@ -216,10 +235,17 @@ const HeroModal = ({
: group === "satker" && satkerName : group === "satker" && satkerName
? "satker-" + satkerName ? "satker-" + satkerName
: "", : "",
locale == "en" locale === "en"
); );
const interstitial = response?.data?.data || [];
setHeroData(interstitial); const banners = response?.data?.data || [];
const enrichedData = banners.map((item: any) => ({
...item,
fileTypeId: item?.fileType?.id ?? null,
}));
setHeroData(enrichedData);
}; };
const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => { const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {

View File

@ -348,12 +348,12 @@ const HeroNew = (props: { group?: string }) => {
initFetch(); initFetch();
}, []); }, []);
// Show hero modal after 20 seconds when website is fully loaded // Show hero modal after 5 seconds when website is fully loaded
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
console.log("Show modal popup"); console.log("Show modal popup");
setShowModal(true); setShowModal(true);
}, 30000); // 30 seconds }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);

View File

@ -112,6 +112,11 @@ export async function getMediaBlastBroadcastList(
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function getMediaBlastBroadCast(id: string) {
const url = `media/blast/broadcast?id=${id}`;
return httpGetInterceptor(url);
}
export async function listDataPopUp( export async function listDataPopUp(
page: number, page: number,
limit: string, limit: string,