From 2d57cd606ae1c66f7389e51f0f8f0dc752703299 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Sun, 8 Jun 2025 16:53:46 +0700 Subject: [PATCH 1/8] feat : fixing calendar view of agenda-settings --- .../agenda-setting/calender-view.tsx | 738 +++++++----------- .../contributor/agenda-setting/page.tsx | 2 +- 2 files changed, 299 insertions(+), 441 deletions(-) diff --git a/app/[locale]/(protected)/contributor/agenda-setting/calender-view.tsx b/app/[locale]/(protected)/contributor/agenda-setting/calender-view.tsx index e2209c2d..21443c40 100644 --- a/app/[locale]/(protected)/contributor/agenda-setting/calender-view.tsx +++ b/app/[locale]/(protected)/contributor/agenda-setting/calender-view.tsx @@ -1,23 +1,19 @@ "use client"; -import React, { useState, useEffect, act } from "react"; -import FullCalendar from "@fullcalendar/react"; // must go before plugins +import React, { useState, useEffect } from "react"; +import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin, { Draggable } from "@fullcalendar/interaction"; import listPlugin from "@fullcalendar/list"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import ExternalDraggingevent from "./dragging-events"; import { Calendar } from "@/components/ui/calendar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Book, CheckCheck, - CheckSquare2, - CheckSquare2Icon, Plus, Timer, - TimerIcon, } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { EventContentArg } from "@fullcalendar/core"; @@ -42,8 +38,8 @@ import { } from "@/components/ui/dialog"; const wait = () => new Promise((resolve) => setTimeout(resolve, 1000)); + interface CalendarViewProps { - // events: CalendarEvent[]; categories: CalendarCategory[]; } @@ -59,12 +55,11 @@ export type CalendarEvent = { extendedProps: { calendar: string; description: string; + isPublish?: boolean; + createdByName?: string; }; }; -const INITIAL_YEAR = dayjs().format("YYYY"); -const INITIAL_MONTH = dayjs().format("M"); - export interface AgendaSettingsAPIResponse { id: number; title: string; @@ -74,24 +69,25 @@ export interface AgendaSettingsAPIResponse { startDate: string; endDate: string; isActive: boolean; + isPublish: boolean; createdAt: string; updatedAt: string; createdById: number | null; } interface YearlyData { - january?: Event[]; - february?: Event[]; - march?: Event[]; - april?: Event[]; - may?: Event[]; - june?: Event[]; - july?: Event[]; - august?: Event[]; - september?: Event[]; - october?: Event[]; - november?: Event[]; - december?: Event[]; + january?: AgendaSettingsAPIResponse[]; + february?: AgendaSettingsAPIResponse[]; + march?: AgendaSettingsAPIResponse[]; + april?: AgendaSettingsAPIResponse[]; + may?: AgendaSettingsAPIResponse[]; + june?: AgendaSettingsAPIResponse[]; + july?: AgendaSettingsAPIResponse[]; + august?: AgendaSettingsAPIResponse[]; + september?: AgendaSettingsAPIResponse[]; + october?: AgendaSettingsAPIResponse[]; + november?: AgendaSettingsAPIResponse[]; + december?: AgendaSettingsAPIResponse[]; } interface MonthCardProps { @@ -100,7 +96,7 @@ interface MonthCardProps { } interface ListItemProps { - item: any; + item: AgendaSettingsAPIResponse; text: string; createdBy: string; isPublish: boolean | null; @@ -109,387 +105,279 @@ interface ListItemProps { } interface APIResponse { - error: boolean; - message: any; - data: AgendaSettingsAPIResponse[] | null; + success: boolean; + message: string; + data: YearlyData | null; + errorCode: string | null; } +type EventType = "0" | "1" | "2" | "3" | "4" | "5"; + +const INITIAL_YEAR = dayjs().format("YYYY"); +const INITIAL_MONTH = dayjs().format("M"); + const CalendarView = ({ categories }: CalendarViewProps) => { - const [selectedCategory, setSelectedCategory] = useState( - null - ); + const [selectedCategory, setSelectedCategory] = useState([]); const [selectedEventDate, setSelectedEventDate] = useState(null); const roleId = Number(getCookiesDecrypt("urie")) || 0; const userLevelId = Number(getCookiesDecrypt("ulie")) || 0; - console.log("roleId", roleId); - console.log("userlevel", userLevelId); - const [apiEvents, setApiEvents] = useState([]); - const [Isloading, setLoading] = useState(false); - const [draggableInitialized, setDraggableInitialized] = - useState(false); + const [calendarEvents, setCalendarEvents] = useState([]); + const [isLoading, setIsLoading] = useState(false); const t = useTranslations("CalendarApp"); - // event canvas state + + // Modal states const [sheetOpen, setSheetOpen] = useState(false); - const [date, setDate] = React.useState(new Date()); + const [date, setDate] = useState(new Date()); const [activeView, setActiveView] = useState("listYear"); - const [yearlyData, setYearlyData] = useState(); - - const TODAY = dayjs().format("yyyy-MM-dd"); - const INITIAL_YEAR = dayjs().format("YYYY"); - const INITIAL_MONTH = dayjs().format("M"); + const [yearlyData, setYearlyData] = useState({}); const [open, setOpen] = useState(false); + const [selectedEventData, setSelectedEventData] = useState(null); - const [selectedYear, setSelectedYear] = useState(new Date()); const [selectedMonth, setSelectedMonth] = useState( dayjs(new Date(Number(INITIAL_YEAR), Number(INITIAL_MONTH) - 1, 1)) ); - const [dragEvents] = useState([ - { title: "New Event Planning", id: "101", tag: "business" }, - { title: "Meeting", id: "102", tag: "meeting" }, - { title: "Generating Reports", id: "103", tag: "holiday" }, - { title: "Create New theme", id: "104", tag: "etc" }, - ]); - - const [calendarEvents, setCalendarEvents] = useState([]); - + // Load events when view or month changes useEffect(() => { - if (activeView == "listYear") { + if (activeView === "listYear") { getYearlyEvents(); } else { getCalendarEvents(); } }, [activeView, selectedMonth]); - const getCalendarEvents = async () => { - console.log("View : ", activeView); - const res = await getAgendaSettingsList( - selectedMonth?.format("YYYY") || INITIAL_YEAR, - selectedMonth.format("M") || INITIAL_MONTH, - "" - ); - console.log("View : API Response:", res); - - if (res?.error) { - error(res?.message); - return; + // Initialize selected categories + useEffect(() => { + if (categories?.length > 0) { + setSelectedCategory(categories.map((c) => c.value)); } + }, [categories]); - const data = res?.data?.data; + const getCalendarEvents = async () => { + setIsLoading(true); + try { + const res = await getAgendaSettingsList( + selectedMonth?.format("YYYY") || INITIAL_YEAR, + selectedMonth.format("M") || INITIAL_MONTH, + "" + ); - if (data) { - console.log("Dataaa : ", data); + if (res?.error) { + error(res?.message || "Failed to fetch events"); + return; + } + + // For monthly view, we need to extract events from the specific month + const monthData = res?.data?.data; + if (monthData) { + const allEvents: CalendarEvent[] = []; // Map API data to the calendarEvents structure - const events = res?.data?.data?.map((event: any) => ({ - id: event.id.toString(), - title: event.title, - createBy: event.createdById, - createdByName: event.createdByName, - start: new Date(event.startDate), - end: new Date(event.endDate), - allDay: true, - extendedProps: { - isPublish: event.isPublish, - calendar: event.agendaType, - description: event.description, + const events = monthData?.map((event: any) => ({ + id: event.id.toString(), + title: event.title, + createBy: event.createdById, createdByName: event.createdByName, - }, - })); + start: new Date(event.startDate), + end: new Date(event.endDate), + allDay: true, + extendedProps: { + isPublish: event.isPublish, + calendar: event.agendaType, + description: event.description, + createdByName: event.createdByName, + }, + })); - console.log("Dataaa event : ", events); - - setCalendarEvents(events); + console.log("Event Data : ", events); + setCalendarEvents(events); + } + } catch (err) { + console.error("Failed to fetch calendar events:", err); + error("Failed to fetch calendar events"); + } finally { + setIsLoading(false); } }; const getYearlyEvents = async () => { - const res = await getAgendaSettingsList( - selectedMonth?.format("YYYY") || INITIAL_YEAR, - "", - "" - ); - if (res?.error) { - error(res.message); - return false; + setIsLoading(true); + try { + const res = await getAgendaSettingsList( + selectedMonth?.format("YYYY") || INITIAL_YEAR, + "", + "" + ); + + if (res?.error) { + error(res?.message || "Failed to fetch yearly events"); + return; + } + + // The API already returns data organized by months + const yearlyData = res?.data?.data; + if (yearlyData) { + setYearlyData(yearlyData); + } + } catch (err) { + console.error("Failed to fetch yearly events:", err); + error("Failed to fetch yearly events"); + } finally { + setIsLoading(false); } - setYearlyData(res?.data?.data); }; - useEffect(() => { - setSelectedCategory(categories?.map((c) => c.value)); - }, [categories]); - - const filteredEvents = calendarEvents?.filter((event) => { + // Filter events based on selected categories + const filteredEvents = calendarEvents.filter((event) => { + if (!selectedCategory.length) return false; + console.log("Event category : ", selectedCategory); + const eventCategories = event.extendedProps.calendar ?.split(",") - .map((val: string) => val.trim()); // agar "1, 2" tetap dianggap benar + .map((val: string) => val.trim()); + + const allCategoryId = ["1", "2", "3", "4", "5"]; + // Cek apakah SEMUA validTypeIds ada di typeIdsInData + const hasAllCategories = allCategoryId.every(categoryId => selectedCategory.includes(categoryId)); return eventCategories?.some((cat: string) => - selectedCategory?.includes(cat) + selectedCategory.includes(cat) || (hasAllCategories && cat == "0") ); }); - const displayedEvents = - filteredEvents?.length > 1 ? filteredEvents : apiEvents; - - useEffect(() => { - setSelectedCategory(categories?.map((c: any) => c.value)); - }, [categories]); - - useEffect(() => { - console.log("Selected categories:", selectedCategory); - }, [selectedCategory]); - - useEffect(() => { - fetchAgendaEvents(); - }, []); - - const fetchAgendaEvents = async (year?: string, month?: string) => { - setLoading(true); - try { - const selectedMonth = new Date(); // Replace with your logic for selected month - const year = selectedMonth.getFullYear().toString(); - const month = (selectedMonth.getMonth() + 1).toString(); - const typeFilter = ""; // Replace with your type filter logic if needed - - const response = await getAgendaSettingsList(year, month, typeFilter); - - if (response?.data && Array.isArray(response?.data)) { - // Transform API data to match CalendarEvent type - const eventsFromAPI: CalendarEvent[] = response?.data?.map((item) => ({ - id: item.id.toString(), - title: item.title, - createBy: "Mabes Polri - Approver", - createdByName: item.createdByName, - isPublish: item.isPublish, - start: new Date(item.startDate), - end: new Date(item.endDate), - allDay: true, - extendedProps: { - calendar: item.agendaType, - description: item.description, - }, - })); - - setApiEvents(eventsFromAPI); - } else { - console.warn("No events found in API response."); - setApiEvents([]); - } - } catch (error) { - console.error("Failed to fetch agenda settings:", error); - setApiEvents([]); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - console.log("Fetched events from API 1:", apiEvents); - }, [apiEvents]); - - useEffect(() => { - console.log("Filtered events based on category 1:", calendarEvents); - }, [filteredEvents, apiEvents]); - - useEffect(() => { - const draggableEl = document.getElementById("external-events"); - - const initDraggable = () => { - if (draggableEl) { - new Draggable(draggableEl, { - itemSelector: ".fc-event", - eventData: function (eventEl) { - let title = eventEl.getAttribute("title"); - let id = eventEl.getAttribute("data"); - let event = dragEvents.find((e) => e.id === id); - let tag = event ? event.tag : ""; - return { - title: title, - id: id, - extendedProps: { - calendar: tag, - }, - }; - }, - }); - } - }; - - if (dragEvents.length > 0) { - initDraggable(); - } - - return () => { - draggableEl?.removeEventListener("mousedown", initDraggable); - }; - }, [dragEvents]); - - // event click + // Event handlers const handleEventClick = (arg: any) => { - console.log("Event Click ", arg); setSelectedEventDate(null); + setSelectedEventData(arg); setSheetOpen(true); - setApiEvents(arg); wait().then(() => (document.body.style.pointerEvents = "auto")); }; - // handle close modal const handleCloseModal = () => { setSheetOpen(false); - setApiEvents([]); + setSelectedEventData(null); setSelectedEventDate(null); }; + const handleDateClick = (arg: any) => { setSheetOpen(true); - console.log("Date :", arg); setSelectedEventDate(arg); - setApiEvents([]); + setSelectedEventData(null); wait().then(() => (document.body.style.pointerEvents = "auto")); }; const handleCategorySelection = (category: string) => { - if (selectedCategory && selectedCategory.includes(category)) { - setSelectedCategory(selectedCategory.filter((c) => c !== category)); - } else { - setSelectedCategory([...(selectedCategory || []), category]); - } - }; - - const renderEventContent = (eventInfo: any) => { - const { title } = eventInfo.event; - const { createdByName, isPublish } = eventInfo.event.extendedProps; - - return ( -
-
-
- {isPublish === true ? : } -

{title}

-
- -

- Created By : {createdByName} -

-
- -
-
-
-
-
-
- ); - }; - - const handleClassName = (arg: EventContentArg) => { - if (arg.event.extendedProps.calendar === "1") { - return "bg-yellow-500 border-none"; - } else if (arg.event.extendedProps.calendar === "2") { - return "bg-blue-400 border-none"; - } else if (arg.event.extendedProps.calendar === "3") { - return "bg-slate-400 border-none"; - } else if (arg.event.extendedProps.calendar === "4") { - return "bg-orange-500 border-none"; - } else if (arg.event.extendedProps.calendar === "5") { - return "bg-green-400 border-none"; - } else { - return "primary"; - } + setSelectedCategory(prev => { + if (prev.includes(category)) { + return prev.filter((c) => c !== category); + } else { + return [...prev, category]; + } + }); }; const handleDateChange = (startDate: string, endDate: string) => { const startDateOfDate = new Date(startDate); - const endDateOfDate = new Date(endDate); - - console.log("Change Date : ", startDateOfDate); - setSelectedMonth(dayjs(startDateOfDate)); }; const handleViewChange = (viewType: string) => { - console.log("Change view : ", viewType); setActiveView(viewType); }; - const months: Array<{ id: keyof YearlyData; label: string }> = [ - { id: "january", label: "Januari" }, - { id: "february", label: "Februari" }, - { id: "march", label: "Maret" }, - { id: "april", label: "April" }, - { id: "may", label: "Mei" }, - { id: "june", label: "Juni" }, - { id: "july", label: "Juli" }, - { id: "august", label: "Agustus" }, - { id: "september", label: "September" }, - { id: "october", label: "Oktober" }, - { id: "november", label: "November" }, - { id: "december", label: "Desember" }, - ]; - - const getEventColor = (type: Event["type"]): string => { - const typeSplit = type.split(","); - const firstType = typeSplit && typeSplit[0]; - const colors: Record = { - "0": "bg-black", - "1": "bg-yellow-500", - "2": "bg-blue-400", - "3": "bg-slate-400", - "4": "bg-orange-500", - "5": "bg-green-400", - }; - console.log("Type : ", colors[firstType]); - return colors[firstType]; - }; - - const getEventColorList = (type: Event["type"]): string[] | null => { - const typeSplit = type.split(","); - const typeList = typeSplit?.slice(1); - - if (typeList.length === 0) return null; - - const colors: Record = { - "0": "bg-black", - "1": "bg-yellow-500", - "2": "bg-blue-400", - "3": "bg-slate-400", - "4": "bg-orange-500", - "5": "bg-green-400", - }; - - const typeListColor = typeList.map((item) => colors[item] || "bg-gray-200"); - - console.log("Type List Color : ", typeListColor); - return typeListColor; - }; - - const handleClickListItem = (item: any) => { + const handleClickListItem = (item: AgendaSettingsAPIResponse) => { const formattedEvent: CalendarEvent = { id: item.id.toString(), title: item.title, start: new Date(item.startDate), end: new Date(item.endDate), - createBy: "Mabes Polri - Approver", // Sesuaikan dengan data yang sebenarnya jika ada + createBy: item.createdById?.toString() || "", createdByName: item.createdByName, isPublish: item.isPublish, allDay: true, extendedProps: { calendar: item.agendaType, description: item.description, + isPublish: item.isPublish, + createdByName: item.createdByName, }, }; - const finalEvent: any = { - event: formattedEvent, - }; - - console.log("Event click custom : ", finalEvent); + const eventArg = { event: formattedEvent }; setSelectedEventDate(null); + setSelectedEventData(eventArg); setSheetOpen(true); - setApiEvents(finalEvent); wait().then(() => (document.body.style.pointerEvents = "auto")); }; + // Utility functions + const getEventColor = (type: EventType): string => { + const typeSplit = type.split(","); + const firstType = typeSplit[0] as EventType; + + const colors: Record = { + "0": "bg-black", + "1": "bg-yellow-500", + "2": "bg-blue-400", + "3": "bg-slate-400", + "4": "bg-orange-500", + "5": "bg-green-400", + }; + + return colors[firstType] || "bg-gray-400"; + }; + + const getEventColorList = (type: EventType): string[] | null => { + const typeSplit = type.split(","); + const typeList = typeSplit.slice(1); + + if (typeList.length === 0) return null; + + const colors: Record = { + "0": "bg-black", + "1": "bg-yellow-500", + "2": "bg-blue-400", + "3": "bg-slate-400", + "4": "bg-orange-500", + "5": "bg-green-400", + }; + + return typeList.map((item) => colors[item as EventType] || "bg-gray-200"); + }; + + const renderEventContent = (eventInfo: any) => { + const { title } = eventInfo.event; + const { createdByName, isPublish, calendar } = eventInfo.event.extendedProps; + const bgColor = getEventColor(calendar); + const colorList = getEventColorList(calendar); + + return ( +
+
+
+ {isPublish === true ? : } +

{title}

+
+

+ Created By: {createdByName} +

+
+ {colorList && colorList.length > 0 && ( +
+ {colorList.map((color: string, index: number) => ( +
+ ))} +
+ )} +
+ ); + }; + + const handleClassName = (arg: EventContentArg) => { + const eventColor = getEventColor(arg.event.extendedProps.calendar); + return eventColor || "primary"; + }; + + // Components const ListItem: React.FC = ({ item, text, @@ -498,55 +386,55 @@ const CalendarView = ({ categories }: CalendarViewProps) => { bgColor, colorList, }) => ( -
handleClickListItem(item)} > -
-
- {isPublish ? : } -

{text}

-
-

Created By: {createdBy}

+
+
+ {isPublish ? : } +

{text}

- +

Created By: {createdBy}

+
+ {colorList && colorList.length > 0 && (
- {colorList?.map((color: string) => ( -
+ {colorList.map((color: string, index: number) => ( +
))}
+ )}
); - const MonthCard: React.FC = (props: any) => { - const { monthId, label } = props; - const events: any = yearlyData?.[monthId]; - const displayedEvents = events?.slice(0, 3); - const hasMoreEvents = events?.length > 3; + const MonthCard: React.FC = ({ monthId, label }) => { + const events = yearlyData[monthId] || []; + const displayedEvents = events.slice(0, 3); + const hasMoreEvents = events.length > 3; + return (

{label}

-
- {events?.length === 0 ? ( + {events.length === 0 ? (

{t("no-data-yet")}

) : ( <> - {displayedEvents?.map((event: any, index: number) => ( + {displayedEvents.map((event, index) => ( ))} - {hasMoreEvents && ( @@ -566,15 +454,15 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
- {events.map((event: any, index: number) => ( + {events.map((event, index) => ( ))}
@@ -590,9 +478,24 @@ const CalendarView = ({ categories }: CalendarViewProps) => { ); }; - const getModalContent = (type: "terkirim" | "diterima") => ( -
- {Isloading ? ( + const months: Array<{ id: keyof YearlyData; label: string }> = [ + { id: "january", label: "Januari" }, + { id: "february", label: "Februari" }, + { id: "march", label: "Maret" }, + { id: "april", label: "April" }, + { id: "may", label: "Mei" }, + { id: "june", label: "Juni" }, + { id: "july", label: "Juli" }, + { id: "august", label: "Agustus" }, + { id: "september", label: "September" }, + { id: "october", label: "Oktober" }, + { id: "november", label: "November" }, + { id: "december", label: "Desember" }, + ]; + + const getModalContent = () => ( +
+ {isLoading ? (

Loading...

) : ( @@ -606,14 +509,12 @@ const CalendarView = ({ categories }: CalendarViewProps) => { - - - - - - + + + + + +
{"1"}{"MIA - 001"}{"23/01/2025 13:00"}{"Daily Issue 25 Januari 2025 "} - {type === "terkirim" ? "Completed" : "Completed"} -
1MIA - 00123/01/2025 13:00Daily Issue 25 Januari 2025Completed
@@ -623,11 +524,11 @@ const CalendarView = ({ categories }: CalendarViewProps) => { return ( <> -
- +
+ - - {roleId == 3 || roleId == 11 || roleId == 2 || roleId == 12 ? ( + + {[3, 11, 2, 12].includes(roleId) && ( - ) : ( - "" )} -
+ + {roleId === 3 && userLevelId === 216 && ( - {roleId === 3 && userLevelId === 216 ? ( - - ) : null} + {t("monitoring-results")} - {getModalContent("terkirim")} + {getModalContent()} -
+ )}
{ - handleDateClick(s); + onSelect={(selectedDate) => { + if (selectedDate) { + setDate(selectedDate); + handleDateClick(selectedDate); + } }} className="rounded-md border w-full p-0 border-none" />
- {/*
-

- {t("shortDesc")} -

- {dragEvents.map((event) => ( - - ))} -
*/} -
+
{t("filter")}
    -
  • +
  • { - if (selectedCategory?.length === categories?.length) { + checked={selectedCategory.length === categories.length} + onCheckedChange={() => { + if (selectedCategory.length === categories.length) { setSelectedCategory([]); } else { - setSelectedCategory(categories?.map((c) => c.value)); + setSelectedCategory(categories.map((c) => c.value)); } }} />
  • - {categories?.map((category) => ( -
  • + {categories.map((category) => ( +
  • handleCategorySelection(category.value)} + checked={selectedCategory.includes(category.value)} + onCheckedChange={() => handleCategorySelection(category.value)} />
  • @@ -709,15 +602,10 @@ const CalendarView = ({ categories }: CalendarViewProps) => { - + { buttonText: "Year", }, }} - events={displayedEvents} + events={filteredEvents} editable={true} rerenderDelay={10} eventDurationEditable={false} @@ -745,70 +633,40 @@ const CalendarView = ({ categories }: CalendarViewProps) => { eventContent={renderEventContent} viewDidMount={(info) => handleViewChange(info.view.type)} datesSet={(info: any) => { - console.log(info); handleDateChange(info.view.currentStart, info.view.currentEnd); handleViewChange(info.view.type); }} - viewClassNames={ - activeView === "listYear" ? "hide-calendar-grid" : "" - } + viewClassNames={activeView === "listYear" ? "hide-calendar-grid" : ""} /> {activeView === "listYear" && (
    -
    - {months.slice(0, 3).map((month) => ( - - ))} -
    - -
    - {months.slice(3, 6).map((month) => ( - - ))} -
    - -
    - {months.slice(6, 9).map((month) => ( - - ))} -
    - -
    - {months.slice(9, 12).map((month) => ( - - ))} -
    + {[0, 3, 6, 9].map((startIndex) => ( +
    + {months.slice(startIndex, startIndex + 3).map((month) => ( + + ))} +
    + ))}
    )}
+ ); }; -export default CalendarView; +export default CalendarView; \ No newline at end of file diff --git a/app/[locale]/(protected)/contributor/agenda-setting/page.tsx b/app/[locale]/(protected)/contributor/agenda-setting/page.tsx index 010f3ead..9bbbcd75 100644 --- a/app/[locale]/(protected)/contributor/agenda-setting/page.tsx +++ b/app/[locale]/(protected)/contributor/agenda-setting/page.tsx @@ -21,7 +21,7 @@ const CalenderPage = () => { const categories = await getCategories(); let valueShowed: string[] = []; if (userLevelNumber == 1) { - valueShowed = ["1", "2", "3", "4", "5"]; + valueShowed = ["0", "1", "2", "3", "4", "5"]; } else if (userLevelNumber == 2 && userLevelId != 761) { valueShowed = ["2", "3"]; } else if ( From 1fc21c8d2bcbacd2c4316b88846081495644a544 Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Sun, 8 Jun 2025 19:06:17 +0800 Subject: [PATCH 2/8] 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

+ ( +