pull main

This commit is contained in:
Sabda Yagra 2025-07-30 23:16:54 +07:00
commit a6683014c2
7 changed files with 457 additions and 322 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";
@ -33,9 +33,9 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Button } from "@/components/ui/button"; 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,319 +69,342 @@ interface PageProps {
}; };
} }
export default function BroadcastCampaignDetail({ params, searchParams }: PageProps) { export default function BroadcastCampaignDetail({
const router = useRouter(); params,
const pathname = usePathname(); searchParams,
const { id, locale } = params; }: PageProps) {
const [getData, setGetData] = useState<CampaignData[]>([]); const router = useRouter();
const [totalPage, setTotalPage] = useState<number>(0); const pathname = usePathname();
const [totalData, setTotalData] = useState<number>(0); const { id, locale } = params;
const [activeTab, setActiveTab] = useState<"sent" | "schedule" | "account-list">("sent"); const [getData, setGetData] = useState<CampaignData[]>([]);
const { page, size } = searchParams; const [totalPage, setTotalPage] = useState<number>(0);
const [totalData, setTotalData] = useState<number>(0);
const [activeTab, setActiveTab] = useState<
"sent" | "schedule" | "account-list"
>("sent");
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]>([
const [startDate, endDate] = dateRange; new Date(),
new Date(),
]);
const [startDate, endDate] = dateRange;
const [startDateString, setStartDateString] = useState<string | undefined>(); const [startDateString, setStartDateString] = useState<string | undefined>();
const [endDateString, setEndDateString] = useState<string | undefined>(); const [endDateString, setEndDateString] = useState<string | undefined>();
// Table state // Table state
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({}); const [rowSelection, setRowSelection] = useState({});
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: parseInt(size || "10"), pageSize: parseInt(size || "10"),
}); });
const pages = page ? parseInt(page) - 1 : 0; const pages = page ? parseInt(page) - 1 : 0;
const currentPage = page ? parseInt(page) : 1; const currentPage = page ? parseInt(page) : 1;
const pageSize = parseInt(size || "10"); const pageSize = parseInt(size || "10");
const isFHD = useMediaQuery({ const isFHD = useMediaQuery({
minWidth: 1920, minWidth: 1920,
}); });
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()}`);
}; };
async function getListPaginationData() { async function getListPaginationData() {
loading(); loading();
console.log("Type : ", typeFilter); console.log("Type : ", typeFilter);
console.log("Date : ", startDateString, endDateString); console.log("Date : ", startDateString, endDateString);
try { try {
const res = await getMediaBlastBroadcastList( const res = await getMediaBlastBroadcastList(
pages, pages,
activeTab === "schedule", activeTab === "schedule",
startDateString || "", startDateString || "",
endDateString || "", endDateString || "",
typeFilter, typeFilter,
id id
); );
close(); close();
if (res?.data?.data) { if (res?.data?.data) {
setupData(res.data.data); setupData(res.data.data);
} }
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
close(); close();
}
} }
}
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);
if (rawData !== undefined) { if (rawData !== undefined) {
const dataContent = rawData?.content; const dataContent = rawData?.content;
const data: CampaignData[] = []; const data: CampaignData[] = [];
dataContent.forEach((element, i) => { dataContent.forEach((element, i) => {
element.no = (currentPage - 1) * pageSize + i + 1; element.no = (currentPage - 1) * pageSize + i + 1;
data.push(element); data.push(element);
}); });
setGetData(data); setGetData(data);
setTotalPage(rawData?.totalPages); setTotalPage(rawData?.totalPages);
setTotalData(rawData?.totalElements); setTotalData(rawData?.totalElements);
}
} }
}
const columns: ColumnDef<CampaignData>[] = [ const columns: ColumnDef<CampaignData>[] = [
{ {
accessorKey: "no", accessorKey: "no",
header: "No", header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>, cell: ({ row }) => <span>{row.getValue("no")}</span>,
}, },
{ {
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}`}
</Link> className="text-dark"
), >
}, <span className="font-weight-bold">
{ {row.original.mediaBlastCampaign?.title}
accessorKey: "subject", </span>
header: "Judul", </Link>
cell: ({ row }) => ( ),
<Link href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`} className="text-dark"> },
<span className="font-weight-bold">{row.getValue("subject")}</span> {
</Link> accessorKey: "subject",
), header: "Judul",
}, cell: ({ row }) => (
{ <Link
accessorKey: "type", href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`}
header: "Tipe", className="text-dark"
cell: ({ row }) => ( >
<div className="text-right text-black"> <span className="font-weight-bold">{row.getValue("subject")}</span>
{row.getValue("type")} </Link>
</div> ),
) },
}, {
{ accessorKey: "type",
accessorKey: "status", header: "Tipe",
header: "Status", cell: ({ row }) => (
cell: ({ row }) => ( <div className="text-right text-black">{row.getValue("type")}</div>
<div className="text-right text-black"> ),
{row.getValue("status")} },
</div> {
) accessorKey: "status",
}, header: "Status",
{ cell: ({ row }) => (
accessorKey: "sendDate", <div className="text-right text-black">{row.getValue("status")}</div>
header: "Tanggal & Waktu", ),
cell: ({ row }) => ( },
<div className="text-black"> {
{row.getValue("sendDate")} accessorKey: "sendDate",
</div> header: "Tanggal & Waktu",
) cell: ({ row }) => (
} <div className="text-black">{row.getValue("sendDate")}</div>
]; ),
},
];
const table = useReactTable({ const table = useReactTable({
data: getData, data: getData,
columns, columns,
onSortingChange: setSorting, onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination, onPaginationChange: setPagination,
state: { state: {
sorting, sorting,
columnFilters, columnFilters,
columnVisibility, columnVisibility,
rowSelection, rowSelection,
pagination, pagination,
}, },
}); });
useEffect(() => { useEffect(() => {
function initState() { function initState() {
if (startDate != null && endDate != null) { if (startDate != null && endDate != null) {
setStartDateString(getOnlyDate(startDate)); setStartDateString(getOnlyDate(startDate));
setEndDateString(getOnlyDate(endDate)); setEndDateString(getOnlyDate(endDate));
} }
} }
console.log('date range', dateRange); console.log("date range", dateRange);
initState(); initState();
}, [calenderState, startDate, endDate]); }, [calenderState, startDate, endDate]);
const handleTypeFilter = (type: string) => { const handleTypeFilter = (type: string) => {
setTypeFilter(type); setTypeFilter(type);
}; };
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
onClick={() => setActiveTab("sent")} onClick={() => setActiveTab("sent")}
size="md" size="md"
className={`hover:text-white ${ className={`hover:text-white ${
activeTab === "sent" activeTab === "sent"
? "bg-indigo-600 text-white " ? "bg-indigo-600 text-white "
: "bg-white text-black " : "bg-white text-black "
}`} }`}
> >
Sent Sent
</Button> </Button>
<Button <Button
onClick={() => setActiveTab("schedule")} onClick={() => setActiveTab("schedule")}
size="md" size="md"
className={`hover:text-white ${ className={`hover:text-white ${
activeTab === "schedule" activeTab === "schedule"
? "bg-indigo-600 text-white " ? "bg-indigo-600 text-white "
: "bg-white text-black " : "bg-white text-black "
}`} }`}
> >
Schedule Schedule
</Button> </Button>
<Button <Button
onClick={() => setActiveTab("account-list")} onClick={() => setActiveTab("account-list")}
size="md" size="md"
className={`hover:text-white ${ className={`hover:text-white ${
activeTab === "account-list" activeTab === "account-list"
? "bg-indigo-600 text-white " ? "bg-indigo-600 text-white "
: "bg-white text-black " : "bg-white text-black "
}`} }`}
> >
List Akun List Akun
</Button> </Button>
</div>
{activeTab === "account-list" ? (
<AccountListTable />
) : (
<>
<div className="broadcast-filter flex flex-column gap-3 mb-4">
<div className="flex flex-row gap-1 border-2 rounded-md w-fit h-fit">
<Button
onClick={() => handleTypeFilter("email")}
className={`hover:text-white ${
typeFilter === "email"
? "bg-black text-white "
: "bg-white text-black "
}`}
size='sm'
>
Email Blast
</Button>
<Button
onClick={() => handleTypeFilter("wa")}
className={`hover:text-white ${
typeFilter === "wa"
? "bg-black text-white "
: "bg-white text-black "
}`}
size='sm'
>
WhatsApp Blast
</Button>
</div>
<div className="dashboard-date-picker">
<div className="mx-6 my-1">
<ReactDatePicker
selectsRange
startDate={startDate}
endDate={endDate}
onChange={(update) => {
setDateRange(update as [Date, Date]);
}}
placeholderText="Pilih Tanggal"
onCalendarClose={() => setCalenderState(!calenderState)}
className="form-control rounded-pill"
/>
</div>
</div>
</div>
<div className="w-full overflow-x-auto">
<Table className="overflow-hidden mt-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
</>
)}
</div>
</div> </div>
);
{activeTab === "account-list" ? (
<AccountListTable />
) : (
<>
<div className="broadcast-filter flex flex-column gap-3 mb-4">
<div className="flex flex-row gap-1 border-2 rounded-md w-fit h-fit">
<Button
onClick={() => handleTypeFilter("email")}
className={`hover:text-white ${
typeFilter === "email"
? "bg-black text-white "
: "bg-white text-black "
}`}
size="sm"
>
Email Blast
</Button>
<Button
onClick={() => handleTypeFilter("wa")}
className={`hover:text-white ${
typeFilter === "wa"
? "bg-black text-white "
: "bg-white text-black "
}`}
size="sm"
>
WhatsApp Blast
</Button>
</div>
<div className="dashboard-date-picker">
<div className="mx-6 my-1">
<ReactDatePicker
selectsRange
startDate={startDate}
endDate={endDate}
onChange={(update) => {
setDateRange(update as [Date, Date]);
}}
placeholderText="Pilih Tanggal"
onCalendarClose={() => setCalenderState(!calenderState)}
className="form-control rounded-pill"
/>
</div>
</div>
</div>
<div className="w-full overflow-x-auto">
<Table className="overflow-hidden mt-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
</>
)}
</div>
</div>
);
} }

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

@ -8,8 +8,8 @@ const ImageCreatePage = async () => {
return ( return (
<div> <div>
<SiteBreadcrumb /> <SiteBreadcrumb />
<div className="space-y-4"> <div className="space-y-4 ">
<FormImage /> <FormImage />
</div> </div>
</div> </div>
); );

View File

@ -8,7 +8,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Link } from "@/i18n/routing"; import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
@ -27,7 +27,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
}) => { }) => {
const t = useTranslations("LandingPage"); const t = useTranslations("LandingPage");
const { login } = useAuth(); const { login } = useAuth();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(true); const [rememberMe, setRememberMe] = useState(true);
const [roles, setRoles] = useState<Role[]>([]); const [roles, setRoles] = useState<Role[]>([]);
@ -210,4 +210,4 @@ export const LoginForm: React.FC<LoginFormProps> = ({
</form> </form>
</div> </div>
); );
}; };

View File

@ -94,39 +94,39 @@ export const OTPForm: React.FC<OTPFormProps> = ({
className="gap-2" className="gap-2"
> >
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot <InputOTPSlot
index={0} index={0}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
<InputOTPSlot <InputOTPSlot
index={1} index={1}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
</InputOTPGroup> </InputOTPGroup>
<InputOTPSeparator /> <InputOTPSeparator />
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot <InputOTPSlot
index={2} index={2}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
<InputOTPSlot <InputOTPSlot
index={3} index={3}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
</InputOTPGroup> </InputOTPGroup>
<InputOTPSeparator /> <InputOTPSeparator />
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot <InputOTPSlot
index={4} index={4}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
<InputOTPSlot <InputOTPSlot
index={5} index={5}
onKeyDown={handleTypeOTP} onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg" className="w-12 h-12 text-lg"
/> />
@ -166,4 +166,4 @@ export const OTPForm: React.FC<OTPFormProps> = ({
</div> </div>
</div> </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

@ -106,17 +106,22 @@ export async function saveMediaBlastBroadcast(data: any) {
} }
export async function getMediaBlastBroadcastList( export async function getMediaBlastBroadcastList(
page: number, page: number,
isScheduled: boolean, isScheduled: boolean,
startDate: string, startDate: string,
endDate: string, endDate: string,
type: string, type: string,
campaignId: string campaignId: string
) { ) {
const url = `media/blast/broadcast/list?enablePage=1&page=${page}&isScheduled=${isScheduled}&type=${type}&startDate=${startDate}&endDate=${endDate}&campaignId=${campaignId}`; const url = `media/blast/broadcast/list?enablePage=1&page=${page}&isScheduled=${isScheduled}&type=${type}&startDate=${startDate}&endDate=${endDate}&campaignId=${campaignId}`;
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,